-
Notifications
You must be signed in to change notification settings - Fork 22
/
index.src.html
1474 lines (1180 loc) · 62.7 KB
/
index.src.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<h1>Private Network Access</h1>
<pre class="metadata">
Status: CG-DRAFT
Group: WICG
ED: https://wicg.github.io/private-network-access/
Repository: wicg/private-network-access
Shortname: private-network-access
Level: 1
Editor: Titouan Rigoudy 124486, Google, titouan@google.com
Former Editor: Mike West 56384, Google, mkwst@google.com
Abstract:
This document specifies modifications to Fetch and HTML which are intended to
mitigate the risks associated with unintentional exposure of devices and
servers on a client's internal network to the web at large.
This specification was previously known as CORS-RFC1918.
Indent: 2
Boilerplate: omit conformance
Markup Shorthands: css off, markdown on
</pre>
<pre class="anchors">
spec: RFC1918; urlPrefix: https://tools.ietf.org/html/rfc1918
type: dfn
text: private address space; url: section-3
spec: HTML; urlPrefix: https://html.spec.whatwg.org/
type: interface
text: Document; url: document;
type: abstract-op
text: create a new browsing context; url: multipage/browsers.html#creating-a-new-browsing-context
text: initialize the Document object; url: multipage/browsing-the-web.html#initialise-the-document-object
text: run a worker; url: multipage/workers.html#run-a-worker
spec: FETCH; urlPrefix: https://fetch.spec.whatwg.org/
type: abstract-op
text: fetch; url: #concept-fetch
spec: FETCH; urlPrefix: https://fetch.spec.whatwg.org/
type: abstract-op
text: HTTP-network fetch; url: #concept-http-network-fetch
spec: FETCH; urlPrefix: https://fetch.spec.whatwg.org/
type: abstract-op
text: HTTP-network-or-cache fetch; url: #concept-http-network-or-cache-fetch
</pre>
<pre class="link-defaults">
spec:fetch; type:dfn; for:/; text:cache entry
spec:fetch; type:dfn; for:/; text:cache entry match
spec:fetch; type:dfn; for:/; text:cors check
spec:fetch; type:dfn; for:/; text:cors-preflight fetch
spec:fetch; type:dfn; for:/; text:cors-preflight cache
spec:fetch; type:dfn; for:/; text:create a new cache entry
spec:fetch; type:dfn; for:/; text:fetch params
spec:fetch; type:dfn; for:/; text:network partition key
spec:fetch; type:dfn; for:/; text:obtain a connection
spec:fetch; type:dfn; for:/; text:request
spec:fetch; type:dfn; for:/; text:response
spec:fetch; type:dfn; for:fetch params; text:request
spec:fetch; type:dfn; for:cache; text:cache match
spec:websockets; type:dfn; for:/; text:establish a websocket connection
</pre>
<pre class="biblio">
{
"CSRF-EXPLOIT-KIT": {
"href": "http://malware.dontneedcoffee.com/2015/05/an-exploit-kit-dedicated-to-csrf.html",
"title": "An Exploit Kit dedicated to CSRF Pharming",
"authors": [ "Kafeine" ]
},
"DRIVE-BY-PHARMING": {
"href": "https://link.springer.com/chapter/10.1007/978-3-540-77048-0_38",
"title": "Drive-By Pharming",
"authors": [ "Sid Stamm", "Zulfikar Ramzan", "Markus Jakobsson" ]
},
"PLEX": {
"href": "https://blog.filippo.io/how-plex-is-doing-https-for-all-its-users/",
"title": "How Plex is doing HTTPS for all its users",
"authors": [ "Filippo Valsorda" ]
},
"SECURE-LOCAL-COMMUNICATION": {
"href": "http://www.w3.org/2015/10/28-local-minutes.html",
"title": "Minutes from 'Secure communication with local network devices': TPAC, 2015"
},
"SOHO-PHARMING": {
"href": "https://331.cybersec.fun/TeamCymruSOHOPharming.pdf",
"title": "SOHO Pharming",
"authors": [ "Team Cymru" ]
},
"AVASTIUM": {
"href": "https://bugs.chromium.org/p/project-zero/issues/detail?id=679",
"title": "Avast: A web-accessible RPC endpoint can launch 'SafeZone' (also called Avastium), a Chromium fork with critical security checks removed."
},
"TREND-MICRO": {
"href": "https://bugs.chromium.org/p/project-zero/issues/detail?id=693",
"title": "TrendMicro node.js HTTP server listening on localhost can execute commands"
},
"IPV4-REGISTRY": {
"href": "https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml",
"title": "IANA IPv4 Special-Purpose Address Registry"
},
"IPV6-REGISTRY": {
"href": "https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml",
"title": "IANA IPv6 Special-Purpose Address Registry"
},
"MIXED-CONTENT-2": {
"href": "https://w3c.github.io/webappsec-mixed-content/level2.html",
"title": "Mixed Content Level 2",
"authors": ["Emily Stark", "Mike West", "Carlos Ibarra Lopez"],
"status": "W3C First Public Working Draft",
"date": "14 October 2020"
},
"LINK-LOCAL-URI": {
"href": "https://www.ietf.org/archive/id/draft-ietf-6man-rfc6874bis-07.html",
"title": "Representing IPv6 Zone Identifiers in Address Literals and Uniform Resource Identifiers",
"authors": ["B. Carpenter", "S. Cheshire", "R. Hinden"],
"status": "Internet-Draft",
"date": "12 April 2023"
}
}
</pre>
<style>
ul.toc ul ul ul {
margin: 0 0 0 2em;
}
ul.toc ul ul ul span.secno {
margin-left: -9em;
}
table {
border-collapse: collapse;
width: 100%;
caption-side: bottom;
}
table caption {
padding: 5px;
}
th, td {
border-width: 1px;
border-style: solid;
padding: 5px 10px;
}
th {
background-color: lightblue;
border-color: black;
}
td {
border-color: grey;
}
</style>
<!--
████ ██ ██ ████████ ████████ ███████
██ ███ ██ ██ ██ ██ ██ ██
██ ████ ██ ██ ██ ██ ██ ██
██ ██ ██ ██ ██ ████████ ██ ██
██ ██ ████ ██ ██ ██ ██ ██
██ ██ ███ ██ ██ ██ ██ ██
████ ██ ██ ██ ██ ██ ███████
-->
<section>
<h2 id="intro">Introduction</h2>
<em>This section is not normative.</em>
Although [[RFC1918]] has specified a distinction between "private" and
"public" internet addresses for over two decades, user agents haven't made
much progress at segregating the one from the other. Websites on the public
internet can make requests to internal devices and servers, which enable a
number of malicious behaviors, including attacks on users' routers like those
documented in [[DRIVE-BY-PHARMING]], [[SOHO-PHARMING]] and
[[CSRF-EXPLOIT-KIT]].
Here, we propose a mitigation against these kinds of attacks that would
require internal devices to explicitly opt-in to requests from the public
internet.
<h3 id="goals">Goals</h3>
The overarching goal is to prevent the user agent from inadvertently enabling
attacks on devices running on a user's local intranet, or services running on
the user's machine directly. For example, we wish to mitigate attacks on:
* Users' routers, as outlined in [[SOHO-PHARMING]]. Note that status quo
CORS protections don't protect against the kinds of attacks discussed here
as they rely only on [=CORS-safelisted methods=] and
[=CORS-safelisted request-headers=]. No preflight is triggered, and the
attacker doesn't actually care about reading the response, as the request
itself is the CSRF attack.
* Software running a web interface on a user's loopback address. For better
or worse, this is becoming a common deployment mechanism for all manner of
applications, and often assumes protections that simply don't exist (see
[[AVASTIUM]] and [[TREND-MICRO]] for recent examples).
<h3 id="examples">Examples</h3>
<h4 id="example-deny-by-default">Secure by Default</h4>
<div class="example">
MegaCorp Inc's routers have a fairly serious CSRF vulnerability which allows
their DNS settings to be altered by navigating to
`http://admin:admin@router.local/set_dns` and passing in various GET
parameters. Oh noes!
Happily, MegaCorp Inc's routers don't have any interest in requests from the
public internet, and didn't take any special effort to enable them. This
greatly mitigates the scope of the vulnerability, as malicious requests will
generate a <a>CORS-preflight request</a>, which the router ignores. Let's
take a closer look:
Given `https://csrf.attack/` that contains the following HTML:
<pre>
<iframe href="https://admin:admin@router.local/set_dns?server1=123.123.123.123">
</iframe>
</pre>
`router.local` will be resolved to the router's address via the magic of
multicast DNS [[RFC6762]], and the user agent will note it as
[=IP address space/private=]. Since `csrf.attack` resolved to a
[=public address=], the request will trigger a [=CORS-preflight request=]:
<pre>
OPTIONS /set_dns?... HTTP/1.1
Host: router.local
<a http-header>Access-Control-Request-Method</a>: GET
<a http-header>Access-Control-Request-Private-Network</a>: true
...
Origin: https://csrf.attack
</pre>
The router will receive this `OPTIONS` request, and has a number of possible
safe responses:
* If it doesn't understand `OPTIONS` at all, it can return a `50X` error.
This will cause the preflight to fail, and the actual `GET` will never
be issued.
* If it does understand `OPTIONS`, it can neglect to include an
<a http-header>`Access-Control-Allow-Private-Network`</a> header in its
response. This will cause the preflight to fail, and the actual `GET`
will never be issued.
* It can crash. Crashing is fairly safe, if inelegant.
</div>
<h4 id="example-opt-in">Opting-In</h4>
<div class="example">
Some of MegaCorp Inc's devices actually need to talk to the public internet
for various reasons. They can explicitly opt-in to receiving requests from
the internet by sending proper CORS headers in response to a
<a>CORS-preflight request</a>.
When a website on the public internet makes a request to the device, the
user agent determines that the requestor is [=IP address space/public=], and
the router is [=IP address space/private=]. This means that requests will
trigger a [=CORS-preflight request=], just as above.
The device can explicitly grant access by sending the right headers in its
response to the preflight request. For the above request, that might look
like:
<pre>
HTTP/1.1 200 OK
...
<a http-header>Access-Control-Allow-Origin</a>: https://public.example.com
<a http-header>Access-Control-Allow-Methods</a>: GET
<a http-header>Access-Control-Allow-Credentials</a>: true
<a http-header>Access-Control-Allow-Private-Network</a>: true
Content-Length: 0
...
</pre>
</div>
<h4 id="shortlinks">Navigation</h4>
<div class="example">
MegaCorp Inc. runs an internal link shortening service at `https://go/`, and
its employees often email such links to each other. The email server is
hosted at a <a>public address</a> in order to ensure that employees can work
even when they're not at the office. How considerate!
Clicking `https://go/*` links from `https://mail.mega.corp/` will trigger a
<a>CORS-preflight request</a>, as it is a request from a <a>public
address</a> to a <a>private address</a>:
<pre>
OPTIONS /short-links-are-short-after-shortening HTTP/1.1
Host: go
<a http-header>Access-Control-Request-Method</a>: GET
<a http-header>Access-Control-Request-Private-Network</a>: true
...
Origin: https://mail.mega.corp
</pre>
In order to ensure that employees can continue to navigate such links as
expected, MegaCorp chooses to allow private network requests:
<pre>
HTTP/1.1 200 OK
...
<a http-header>Access-Control-Allow-Origin</a>: https://mail.mega.corp
<a http-header>Access-Control-Allow-Methods</a>: GET
<a http-header>Access-Control-Allow-Credentials</a>: true
<a http-header>Access-Control-Allow-Private-Network</a>: true
Content-Length: 0
...
</pre>
MegaCorp's leak-prevention department is worried, though, that this access
will allow external folks to read the location of any redirect that the
shortener would return. They're more or less resigned to the fact that
`https://go/shortlink` will leak, but would be sad indeed if the target
(`https://sekrits/super-sekrit-project-with-super-sekrit-partner`) leaked
as well.
MegaCorp's shortlink engineers are careful to avoid this potential failure
by returning CORS headers <em>only</em> for the preflight. The "real"
navigation doesn't require CORS headers, and they don't actually want to
support cross-origin requests as being CORS-same-origin:
<pre>
// Request:
GET /short-links-are-short-after-shortening HTTP/1.1
Host: go
...
// Response:
HTTP/1.1 301 Moved Permanently
...
Location: https://sekrits/super-sekrit-project-with-super-sekrit-partner
</pre>
The navigation will proceed normally, but `mail.mega.corp` won't be
considered CORS-same-origin with the response.
</div>
<h4 id="example-mixed-content">Mixed Content</h4>
<div class="example">
Some of MegaCorp Inc's devices lack unique origins, preventing them from
connecting through secure channels (e.g., HTTPS). However, these devices may
still want to communicate with the public websites. They can opt-in to an
insecure connection with secure public websites if explicitly allowed by
users.
When a website with a [=potentially trustworthy origin=] on the public
internet requests data from the device, the user agent recognizes the
requestor as [=IP address space/public=], and the device as
[=IP address space/private=] (not a [=potentially trustworthy origin=]).
This triggers both a [=CORS-preflight request=] and a permission prompt to
the user (after receiving the correct preflight response).
Website need to explicitly claim the {{IPAddressSpace}} as a `fetch()` API
option:
<pre highlight="js">
fetch("http://router.local/ping", {
targetAddressSpace: "private",
});
</pre>
The device can grant access by explicitly indicating permission and provide
a unique device ID and a user-friendly device name in the preflight response
headers. An example response to the above request:
<pre>
HTTP/1.1 200 OK
...
<a http-header>Access-Control-Allow-Origin</a>: https://mail.mega.corp
<a http-header>Access-Control-Allow-Methods</a>: GET
<a http-header>Access-Control-Allow-Credentials</a>: true
<a http-header>Access-Control-Allow-Private-Network</a>: true
<a http-header>Private-Network-Access-ID</a>: 01:23:45:67:89:0A
<a http-header>Private-Network-Access-Name</a>: userA's MegaCorp device
Content-Length: 0
...
</pre>
A permission prompt will appear, displaying the ID and name from the device
header. If the user grants permission, the request will proceed.
</div>
</section>
<section>
<h2 id="framework">Framework</h2>
<h3 id="ip-address-space-heading">IP Address Space</h3>
Define {{IPAddressSpace}} as follows:
<pre class="idl">
enum IPAddressSpace { "public", "private", "local" };
</pre>
Every IP address belongs to an
<dfn export local-lt="address space">IP address space</dfn>, which can be one
of three different values:
1. <dfn for="IP address space" export>local</dfn>: contains the local
host only. In other words, addresses whose target differs for every
device.
1. <dfn for="IP address space" export>private</dfn>: contains
addresses that have meaning only within the current network. In other
words, addresses whose target differs based on network position.
1. <dfn for="IP address space" export>public</dfn>: contains all
other addresses. In other words, addresses whose target is the same for
all devices globally on the IP network.
For convenience, we additionally define the following terms:
1. A <dfn>local address</dfn> is an IP address whose [=/IP address space=] is
[=IP address space/local=].
1. A <dfn>private address</dfn> is an IP address whose [=/IP address space=]
is [=IP address space/private=].
1. A <dfn>public address</dfn> is an IP address whose [=/IP address space=]
is [=IP address space/public=].
An [=/IP address space=] |lhs| is
<dfn for="IP address space" export>less public</dfn> than an
[=/IP address space=] |rhs| if any of the following conditions holds true:
1. |lhs| is [=IP address space/local=] and |rhs| is either
[=IP address space/private=] or [=IP address space/public=].
1. |lhs| is [=IP address space/private=] and |rhs| is
[=IP address space/public=].
To <dfn export>determine the IP address space</dfn> of an IP address
|address|, run the following steps:
1. If |address| belongs to the `::ffff:0:0/96` "IPv4-mapped Address"
address block, then replace |address| with its embedded IPv4 address.
1. For each |row| in the
<a href="#non-public-ip-address-blocks">Non-public IP address blocks"</a>
table:
1. If |address| belongs to |row|'s address block, return |row|'s
address space.
1. Return [=IP address space/public=].
<table id="non-public-ip-address-blocks">
<caption>Non-public IP address blocks</caption>
<thead>
<tr>
<th>Address block</th>
<th>Name</th>
<th>Reference</th>
<th>Address space</th>
</tr>
</thead>
<tbody>
<tr>
<td>`127.0.0.0/8`</td>
<td>IPv4 Loopback</td>
<td>[[RFC1122]]</td>
<td>[=IP address space/local=]</td>
</tr>
<tr>
<td>`10.0.0.0/8`</td>
<td>Private Use</td>
<td>[[RFC1918]]</td>
<td>[=IP address space/private=]</td>
</tr>
<tr>
<td>`100.64.0.0/10`</td>
<td>Carrier-Grade NAT</td>
<td>[[RFC6598]]</td>
<td>[=IP address space/private=]</td>
</tr>
<tr>
<td>`172.16.0.0/12`</td>
<td>Private Use</td>
<td>[[RFC1918]]</td>
<td>[=IP address space/private=]</td>
</tr>
<tr>
<td>`192.168.0.0/16`</td>
<td>Private Use</td>
<td>[[RFC1918]]</td>
<td>[=IP address space/private=]</td>
</tr>
<tr>
<td>`198.18.0.0/15`</td>
<td>Benchmarking</td>
<td>[[RFC2544]]</td>
<td>[=IP address space/local=]</td>
</tr>
<tr>
<td>`169.254.0.0/16`</td>
<td>Link Local</td>
<td>[[RFC3927]]</td>
<td>[=IP address space/private=]</td>
</tr>
<tr>
<td>`::1/128`</td>
<td>IPv6 Loopback</td>
<td>[[RFC4291]]</td>
<td>[=IP address space/local=]</td>
</tr>
<tr>
<td>`fc00::/7`</td>
<td>Unique Local</td>
<td>[[RFC4193]]</td>
<td>[=IP address space/private=]</td>
</tr>
<tr>
<td>`fe80::/10`</td>
<td>Link-Local Unicast</td>
<td>[[RFC4291]]</td>
<td>[=IP address space/private=]</td>
</tr>
<tr>
<td>`::ffff:0:0/96`</td>
<td>IPv4-mapped</td>
<td>[[RFC4291]]</td>
<td>see mapped IPv4 address</td>
</tr>
</tbody>
</table>
User Agents MAY allow certain IP address blocks' [=address space=] to be
overridden through administrator or user configuration. This could prove
useful to protect e.g. IPv6 intranets where most IP addresses are considered
[=IP address space/public=] per the algorithm above, by instead configuring
user agents to treat the intranet as [=IP address space/private=].
Note: Link-local IP addresses such as `169.254.0.0/16` are considered
[=IP address space/private=], since such addresses can identify the same
target for all devices on a network link. A previous version of this
specification considered them to be [=IP address space/local=] instead.
Note: Link-local IP addresses lose their meaning if shared across links. This
is not fundamentally different from non-public IP addresses, which all have
some degree of locality beyond which they become ambiguous, but it does
present a particular risk of confused deputy issues. [[LINK-LOCAL-URI]]
attempts to solve this problem by defining a syntax for link-local IP
addresses in URIs.
Note: The contents of each [=/IP address space=] were at one point determined
in accordance with the IANA Special-Purpose Address Registries
([[IPV4-REGISTRY]] and [[IPV6-REGISTRY]]) and the `Globally Reachable` bit
defined therein. This turned out to be an inaccurate signal for our uses, as
described in
[spec issue #50](https://github.com/WICG/private-network-access/issues/50).
ISSUE(36): Remove the special case for IPv4-mapped IPv6 addresses once access
to these addresses is blocked entirely.
<h3 id="private-network-request-heading">Private Network Request</h3>
A [=request=] (|request|) is a <dfn export>private network request</dfn>
if |request|'s [=request/current url=]'s {{URL/host}} maps to an IP address
whose [=/IP address space=] is [=IP address space/less public=] than
|request|'s [=request/policy container=]'s
[=policy container/IP address space=].
The classification of IP addresses into three broad [=address spaces=] is an
imperfect and theoretically-unsound approach. It is a proxy used to determine
whether two network endpoints should be allowed to communicate freely or not,
in other words whether endpoint A is reachable from endpoint B without
pivoting through the user agent on endpoint C.
This approach has some flaws:
- false positives: an intranet server with a [=public address=] might not be
able to directy issue requests to another server on the same intranet with
a [=private address=].
- false negatives: a client connected to two different
[=IP address space/private=] networks, say a home network and a VPN, might
allow a website served from the VPN to access devices on the home network.
See also the issue below.
Even so, this specification aims to offer a pragmatic solution to a security
issue that broadly affects most users of the Web whose network configurations
are not so complex.
ISSUE(39): The definition of [=private network requests=] could be expanded to
cover all cross-origin requests for which the [=request/current url=]'s
{{URL/host}} maps to an IP address whose [=/IP address space=] is not
[=IP address space/public=]. This would prevent a malicious server on the
private network from attacking other servers. The effort require to ship such
a change is not deemed worth the payoff for now. This can be shipped as an
incremental improvement later on.
NOTE: Some [=private network requests=] are more challenging to secure than
others. See [[#rollout-difficulties]] for more details.
<h3 id="headers">Additional CORS Headers</h3>
The <dfn export http-header>`Access-Control-Request-Private-Network`</dfn>
indicates that the [=request=] is a [=private network request=].
The <dfn export http-header>`Access-Control-Allow-Private-Network`</dfn>
indicates that a resource can be safely shared with external networks.
Note: These headers were briefly specified as
`Access-Control-Request-Local-Network` and
`Access-Control-Allow-Local-Network`, but this decision was reversed due to
its compatibility impact. See
[issue #91](https://github.com/WICG/private-network-access/issues/91) for
details.
The <dfn export http-header>`Private-Network-Access-Name`</dfn> attempts to
provide users a human friendly name in the private network access
permission prompt.
The <dfn export http-header>`Private-Network-Access-ID`</dfn> header is used
in the {{PrivateNetworkAccessPermissionDescriptor}} to identify identical
devices across IP addresses.
<h3 id="csp">The `treat-as-public-address` Content Security Policy Directive</h3>
The <dfn>treat-as-public-address</dfn> directive instructs the user agent to
treat a document as though it was served from a [=public address=], even if
it was actually served from a [=private address=] or a [=local address=]. That
is, it is a mechanism by which non-public documents may drop the privilege to
contact other non-public documents without a preflight.
The directive's syntax is described by the following ABNF grammar:
<pre dfn-type="grammar" link-type="grammar">
directive-name = "treat-as-public-address"
directive-value = ""
</pre>
This directive has no reporting requirements; it will be ignored entirely when
delivered in a `Content-Security-Policy-Report-Only` header, or within
a <{meta}> element.
This directive's [=directive/initialization=] algorithm is as follows. Given
an [=environment settings object=] (|context|), a `Response` (|response|), and
a `policy` (|policy|):
1. Set |context|'s [=environment settings object/policy container=]'s
[=policy container/IP address space=] to [=IP address space/public=] if
|policy|'s [=policy/disposition=] is "`enforce`".
<h3 id="feature-detect">Feature Detection</h3>
A previous version of this specification proposed adding an `addressSpace`
enum property to {{Document}} and {{WorkerGlobalScope}}, but it was removed
due to fingerprinting concerns (see
[issue #21](https://github.com/WICG/private-network-access/issues/21)).
Documents should not behave differently or not based on whether the UA
implements this specification or not - all documents should assume it does.
<h3 id="permission-prompt">Permission Prompt</h3>
Following the discussions in
[[Issue#23](https://github.com/WICG/private-network-access/issues/23)], the
private network access permission prompt is introduced to relax mixed content
checks.
The goal of the permission is to allow communication from public websites to
local network servers over HTTP, which would otherwise be prevented by the
<a href="#secure-context-restriction">secure context restriction</a> and mixed
content checks. Migrating private network servers to HTTPS has indeed proven
to be often difficult, even sometimes impossible.
A new parameter is added to the `fetch()` options bag:
<pre highlight="js">
fetch("http://router.local/ping", {
targetAddressSpace: "private",
});
</pre>
This instructs the browser to allow the fetch even though the scheme is
non-secure and obtain a connection to the target server. The new `fetch()` API
is backward-compatible.
Note that this feature cannot be abused to bypass mixed content in general.
If the remote IP address does not belong to the IP address space specified as
the `targetAddressSpace` option value, then the request is failed. If it does
belong, then a CORS preflight request is sent. The target server then responds
with a CORS preflight response, augmented with the following two headers:
<pre>
Private-Network-Access-Name: <some human-readable device self-identification>
Private-Network-Access-ID: <some unique and stable machine-readable ID, such as a MAC address>
</pre>
For example:
<pre>
Private-Network-Access-Name: "My Smart Toothbrush"
Private-Network-Access-ID: "01:23:45:67:89:0A"
</pre>
<a http-header>`Private-Network-Access-ID`</a> should be a 48-bit value presented as 6
hexadecimal bytes separated by colons.
<a http-header>`Private-Network-Access-Name`</a> should be a valid name which is a string that matches
the [ECMAScript] regexp /^[a-z0-9_-.]+$/. 248 is the maximum number of UTF-8
code units in the name.
A prompt is then shown to the user asking for permission to access the target
device. The `-Name` header is used to present a friendly string to the user
instead of, or in addition to, an origin (often a raw IP literal). The `-ID`
header is used to key the permission and recognize the device across IP
addresses. Indeed, widespread use of DHCP means that devices are likely to
change IP addresses regularly, and we would like to avoid both cross-device
confusion and permission fatigue.
If the user decides to grant the permission, then the fetch continues. If not,
it fails. The permission is then persisted. The next document belonging to the
same initiator origin that declares its intent to access the same server
(perhaps at a different origin, if using a raw IP address) does not trigger a
permission prompt. The initial CORS preflight response carries the same ID,
and the browser recognizes that the document already has permission to access
the server.
If there's no existing `-Name` or `-ID`, the prompt is shown only with the IP
address. If the user decides to grant the permission, then the fetch continues.
The permission stores as an ephemeral permission and only persists for the
current window process.
</section>
<!-- Big Text: Integrations -->
<section>
<h2 id="integrations">Integrations</h2>
<em>This section is non-normative.</em>
This document proposes a number of modifications to other specifications in
order to implement the mitigations sketched out in the examples above. These
integrations are outlined here for clarity, but the external documents are the
normative references.
<h3 id="secure-context-restriction">Secure context restriction</h4>
UAs must not allow <a lt="secure context">non-secure</a>
[=IP address space/public=] contexts to request resources from
[=private addresses=], even if the private server would opt-in to such a
request via a preflight. Making requests to [=IP address space/private=]
resources presents risks which are mitigated by ensuring the integrity of the
[=request/client=] which initiates the request. In particular, network
attackers should not be able to trivially exploit an endpoint's consent to a
non-secure origin.
Mixed content checks [[MIXED-CONTENT-2]] prevent secure contexts from making
requests over HTTP, so this restriction would seem to require that private
network servers migrate to HTTPS. This is often difficult to impossible. A
new permission prompt is introduced to allow secure contexts to make
requests over HTTP to the private network anyway, given user consent.
<h3 id="integration-permissions">Integration with Permissions </h3>
This document defines a [=powerful feature=] identified by the [=powerful
feature/name=] <dfn export permission>`"private-network-access"`</dfn>. It
overrides the following type:
<dl>
<dt>[=powerful feature/permission descriptor type=]</dt>
<dd>
The [=powerful feature/permission descriptor type=] of the
<a permission>`"private-network-access"`</a> feature is defined by the
following WebIDL interface that [=dictionary/inherits=] from the
default [=powerful feature/permission descriptor type=]:
<pre class="idl">
dictionary PrivateNetworkAccessPermissionDescriptor
: PermissionDescriptor {
DOMString id;
};
</pre>
</dd>
</dl>
<h3 id="integration-mixed-content">Integration with Mixed Content</h3>
The [=Should fetching request be blocked as mixed content?=] is amended to add
the following condition to one of the **allowed** conditions:
1. |request|'s [=request/origin=] is not a [=potentially trustworthy origin=],
and |request|'s [=request/target IP address space=] is [=IP address
space/private=] or [=IP address space/local=].
<h3 id="integration-fetch">Integration with Fetch</h3>
This document proposes a few changes to Fetch, with the following implication:
[=private network requests=] are only allowed if their [=request/client=] is
a [=secure context=] **and** a [=CORS-preflight request=] to the target origin
is successful. If the request would have been blocked as mixed content, it can
be allowed as long as the website states its intention to access the private
network, and users give permission.
Note: This includes navigations. These can indeed be used to trigger CSRF
attacks, albeit with less subtlety than with subresource requests.
Note: [[FETCH]] does not yet integrate the details of DNS resolution into the
[=/Fetch=] algorithm, though it does define an [=obtain a connection=]
algorithm which is enough for this specification. Private Network Access
checks are applied to the newly-obtained connection. Given complexities such
as Happy Eyeballs ([[RFC6555 obsolete]], [[RFC8305]]), these checks might pass
or fail non-deterministically for hosts with multiple IP addresses that
straddle [=/IP address space=] boundaries.
<h4 id="cors-preflight">CORS preflight</h4>
The [=HTTP fetch=] algorithm should be adjusted to ensure that a preflight is
triggered for all [=private network requests=] initiated from
[=secure contexts=].
The main issue here is again that the response's
[=response/IP address space=] is not known until a connection is obtained in
[$HTTP-network fetch$], which is layered under [=CORS-preflight fetch=].
<h4 id="fetching">Fetching</h4>
What follows is a sketch of a potential solution:
1. [=Connection=] objects are given a new
<dfn export for="connection">IP address space</dfn> property, initially
null. This applies to WebSocket connections too.
1. A new step is added to the [=obtain a connection=] algorithm immediately
before appending |connection| to the user agent's [=connection pool=]:
1. Set |connection|'s [=connection/IP address space=] to
the result of running the [=determine the IP address space=] algorithm
on the IP address of |connection|'s remote endpoint.
ISSUE(33): The remote endpoint concept is not specified in [[FETCH]]
yet, hence this is still handwaving to some extent.
1. [=Request=] objects are given a new <dfn for="request" export>target IP
address space</dfn> property, initially null.
1. [=Response=] objects are given a new
<dfn export for="response">IP address space</dfn> property, whose value is
an [=/IP address space=], initially null.
1. Define a new <dfn export>Private Network Access check</dfn> algorithm.
Given a [=request=] |request| and a [=connection=] |connection|:
1. If |request|'s [=request/origin=] is a [=potentially trustworthy
origin=] and |request|’s [=request/current URL=]’s [=request/origin=]
is [=same origin=] with |request|’s [=request/origin=], then return
null.
1. If |request|'s [=request/policy container=] is null, then return null.
NOTE: If |request|'s [=request/policy container=] is null, then PNA
checks do not apply to |request|. Users of the [=fetch=] algorithm
should take care to either set |request|'s [=request/client=] to an
[=environment settings object=] with a non-null [=environment settings
object/policy container=] and let [=fetch=] initialize |request|'s
[=request/policy container=] accordingly, or to directly set
|request|'s [=request/policy container=] to a non-null value.
1. If |request|'s [=request/target IP address space=] is not null, then:
1. [=Assert=]: |request|'s [=request/target IP address space=] is not
[=IP address space/public=].
1. If |connection|'s [=connection/IP address space=] is not equal to
then |request|'s [=request/target IP address space=], then return
a [=network error=].
1. Return null.
1. If |connection|'s [=connection/IP address space=] is
[=IP address space/less public=] than |request|'s [=request/policy
container=]'s [=policy container/IP address space=], then:
1. Let |error| be a [=network error=].
1. If |request|'s [=request/client=] is not a [=secure context=]
(including if it is null), then return |error|.
1. Set |error|'s [=response/IP address space=] property to
|connection|'s [=connection/IP address space=].
1. Return |error|.
1. Return null.
1. The [$fetch$] algorithm is amended to add the following step immediately
after |request|'s [=request/policy container=] is set:
1. If |request|'s [=request/target IP address space=] is [=IP address
space/public=], then return a [=network error=].
1. The [$HTTP-network fetch$] algorithm is amended to add 3 new steps right
after checking that the newly-obtained <var ignore>connection</var> is not
failure:
1. Set |response|'s [=response/IP address space=] to
|connection|'s [=connection/IP address space=].
1. Let |privateNetworkAccessCheckResult| be the result of running
[=Private Network Access check=] for |fetchParams|' [=request=] and
|connection|.
1. If |privateNetworkAccessCheckResult| is a [=network error=], return
|privateNetworkAccessCheckResult|.
1. Define a new algorithm to <dfn>determine the preflight mode</dfn>, given a
[=request=] |request| and a boolean |makeCORSPreflight|:
1. If |makeCORSPreflight| is true and one of these conditions is true:
* There is no method cache entry match for |request|'s
[=request/method=] using |request|, and either |request|'s
[=request/method=] is not a [=CORS-safelisted method=] or
|request|'s [=request/use-CORS-preflight flag=] is set.
* There is at least one [=list/item=] in the CORS-unsafe
request-header names with |request|'s [=request/header list=] for
which there is no header-name cache entry match using |request|.
Then:
1. If |request|'s [=request/target IP address space=] is not
null, then return "cors+pna".
1. Otherwise, return "cors".
1. If |request|'s [=request/target IP address space=] is not null, then
return "pna".
1. Otherwise, return "none".
1. Define a new algorithm called <dfn>HTTP-no-service-worker fetch</dfn>
based on the existing steps in [=HTTP fetch=] that are run if |response|
is still null after handling the fetch via service workers, and amend
those slightly as follows:
1. Let |preflightMode| be the result of invoking [=determine the
preflight mode=] given |request| and |makeCORSPreflight|.
1. Replace the entire condition "If <var ignore>makeCORSPreflight</var>
is true and ..., Then:" with:
1. If |preflightMode| is not "none", then:
1. Replace "running [=CORS-preflight fetch=] given |request|" with
"running [=CORS-preflight fetch=] given |request| and
|preflightMode|"
1. Immediately after running [=CORS-preflight fetch=]:
1. If |preflightResponse| is a [=network error=]:
1. If |preflightResponse|'s [=response/IP address space=] is
null, return |preflightResponse|.
1. Set |request|'s [=request/target IP address space=] to
|preflightResponse|'s [=response/IP address space=].
1. Return the result of running [=HTTP-no-service-worker fetch=]
given |fetchParams|.
1. Immediately after running [$HTTP-network-or-cache fetch$]:
1. If |response| is a [=network error=] and |response|'s
[=response/IP address space=] is non-null, then:
1. Set |request|'s [=request/target IP address space=] to
|preflightResponse|'s [=response/IP address space=].
1. Return the result of running [=HTTP-no-service-worker fetch=]
given |fetchParams|.
Note: Because |request|'s [=request/target IP address space=] is set to a
non-null value when recursing, this recursion can go at most 1 level deep.
1. The [=CORS-preflight fetch=] algorithm is adjusted to take a new parameter
|preflightMode| (default "cors"), and handle the new headers as follows:
1. Only append \``Accept`\` and
\`<a http-header>`Access-Control-Request-Headers`</a>\` to
<var ignore>preflight</var>'s [=request/header list=] if
|preflightMode| is true.
1. Immediately before running [$HTTP-network-or-cache fetch$]:
1. If |request|'s [=request/target IP address space=] is not null,
then:
1. [=header list/Set=]
"<a http-header>`Access-Control-Request-Private-Network`</a>"
to "`true`" in <var ignore>preflight</var>'s
[=request/header list=].
1. Immediately after the [=CORS check=]:
1. If |preflightMode| is "pna" or "cors+pna",
1. [=Assert=]: |request|'s [=request/target IP address space=] is
not null.
1. Let |allow| be the result of [=extracting header list values=]
given
"<a http-header>`Access-Control-Allow-Private-Network`</a>"
and |response|'s [=response/header list=].
1. If |allow| is not "`true`", return a [=network error=].
1. Let |requestWithoutTargetIpAddressSpace| be a copy of |request|
but set its [=request/target IP address space=] to be null.
1. If
<a lt="should fetching request be blocked as mixed content?">
should fetching |requestWithoutTargetIpAddressSpace| be
blocked as mixed content
</a> returns **allowed**, return null.