-
Notifications
You must be signed in to change notification settings - Fork 23
Expand file tree
/
Copy pathindex.bs
More file actions
1224 lines (983 loc) · 54.3 KB
/
Copy pathindex.bs
File metadata and controls
1224 lines (983 loc) · 54.3 KB
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
<pre class='metadata'>
Title: Local Network Access
Shortname: LNA
Level: None
Status: w3c/CG-DRAFT
Repository: WICG/local-network-access
URL: https://wicg.github.io/local-network-access/
Editor: Chris Thompson, Google https://google.com, cthomp@google.com
Editor: Hubert Chao, Google https://google.com, hchao@google.com
Abstract: Restrict access to the users' local network with a new permission
Markup Shorthands: markdown yes, css no
Complain About: accidental-2119 yes, missing-example-ids yes
Assume Explicit For: yes
Die On: warning
WPT Path Prefix: TODO-API-LABEL
WPT Display: closed
Include MDN Panels: if possible
Include Can I Use Panels: yes
Indent: 2
</pre>
<pre class=link-defaults>
spec:dom; type:dfn; text:document
</pre>
<pre class="anchors">
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: dfn
text: HTTP-network fetch; url: #concept-http-network-fetch
spec: FETCH; urlPrefix: https://fetch.spec.whatwg.org/;
type: dfn;
text: HTTP-network-or-cache fetch; url: #concept-http-network-or-cache-fetch
spec: FETCH; urlPrefix: https://fetch.spec.whatwg.org/;
type: dfn;
text: create a connection; url: #create-a-connection
spec: FETCH; urlPrefix: https://fetch.spec.whatwg.org/;
type: dfn;
text: obtain a connection; url: #concept-connection-obtain
url: https://websockets.spec.whatwg.org/#concept-websocket-connection-obtain; type: dfn; text: obtain a Websocket connection
spec: WEBSOCKET; urlPrefix: https://websockets.spec.whatwg.org/
type: abstract-op;
text: WebSocket opening handshake; url: #websocket-opening-handshake
spec: HTML; urlPrefix: https://html.spec.whatwg.org/multipage/webappapis.html
type: dfn; for: global object
text: associated Document; url: #concept-document-window
</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" ]
},
"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"
},
"LET-LOCALHOST-BE-LOCALHOST": {
"href": "https://datatracker.ietf.org/doc/html/draft-ietf-dnsop-let-localhost-be-localhost",
"title": "Let 'localhost' be localhost."
}
}
</pre>
<style>
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>
# Introduction # {#intro}
*This section is not normative.*
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 local 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]].
Local Network Access aims to prevent these undesired requests to insecure
devices on the local network. This is achieved by deprecating direct access to
local IP addresses from public websites, and instead requiring that the user
grants permission to the initiating website to make connections to their local
network.
Note: This proposal builds on top of Chrome's previously paused
[[PRIVATE-NETWORK-ACCESS obsolete]] work but differs by gating access on a permission
rather than via preflight requests.
## Goals ## {#goals}
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 CORS preflight is triggered, and the attacker doesn’t
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).
There should be a well-lit path to allow these requests when the user is both
expecting and explicitly allowing the local network access requests to occur.
For example, a user logged in to [plex.tv](https://plex.tv) may want to allow
the site to connect to their local media server to directly load media content
over the local network instead of routing through remote servers. See
[[#examples]] below for more examples.
## Non-goals ## {#non-goals}
This spec does not attempt to make it easier to use HTTPS connections on local
network devices. While this would be a useful goal, solving this problem is out
of scope for this specification
## Examples ## {#examples}
### User granting permission ### {#example-user-granting-permission}
<div class="example" id="example-printer-support">
Alice is at home on her laptop browsing the internet. She has a printer on her
local network built by Acme Printing Company that is running a simple HTTP
server. Alice is having a problem with the printer not properly functioning.
Alice goes to Acme Printing Company's web site to help diagnose the problem.
Acme Printing Company's web site tells Alice that it can connect to the printer
to look at the diagnostic output of the printer. Alice's browser asks Alice to
allow https://support.acmeprintingcompany.com to connect to local devices on
her network. Alice grants permission for
https://support.acmeprintingcompany.com to connect to local devices on her
network, and https://support.acmeprintingcompany.com connects to her local
printer's diagnostic output, and tells Alice that a part is malfunctioning on
the printer and needs to be replaced.
</div>
### User denying permission ### {#example-user-denying-permission}
<div class="example" id="example-printer-evil">
Alice continues browsing online to find the best price for the replacement part
on her printer. While looking at a general tech support forum, she suddenly
gets a permission request in her browser for https://printersupport.evil.com to
connect to local devices on her local network. Being suspicious of why
https://printersupport.evil.com would need to connect to local devices, she
denies the permission request.
</div>
### New device configuration ### {#example-new-device-configuration}
<div class="example" id="example-printer-setup">
Instead of replacing the part on the printer, Alice decides instead to buy a
new printer from Beta Manufacturing. Upon plugging in the printer and
connecting it to her local network, Alice follows the instructions and goes to
https://setup.betaprinters.com on her laptop. Upon opening the site, she sees a
button that will help her set up the printer defaults. Hitting the button, she
gets a permission prompt asking for permission for
https://setup.betaprinters.com to connect to her local devices, which she
accepts.
</div>
### App-based sign in ### {#example-app-based-sign-in}
<div class="example" id="example-local-login">
Alice uses their personal device for a limited set of tasks at work.
Their employer doesn't require device management for these basic tasks,
but they do require an authentication app to be installed, which provides
a strong credential and a limited set of device context.
When attempting to access a work site, Alice is redirected to their
corporate single sign on service, login.myco.com, and is asked to sign in
using the app. The website makes a fetch request to http://localhost:45678,
the address used by the local app. Alice sees a prompt asking for
permission for https://login.myco.com to connect with a local device, which
they accept. The app is invoked, and Alice is asked to authenticate to the app
using her device unlock.
</div>
# Framework # {#framework}
## IP Address Space ## {#ip-address-space-section}
Define {{IPAddressSpace}} as follows:
<pre class=idl>
enum IPAddressSpace { "public", "local", "loopback" };
</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>loopback</dfn>: Includes loopback addresses,
which are only accessible on the local host. In other words, addresses whose
target differs for every device.
1. <dfn for="IP address space" export>local</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>loopback address</dfn> is an IP address whose [=/IP address space=]
is [=IP address space/loopback=].
1. A <dfn>local address</dfn> is an IP address whose [=/IP address space=] is
[=IP address space/local=].
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/loopback=] and |rhs| is either
[=IP address space/local=] or [=IP address space/public=].
1. |lhs| is [=IP address space/local=] 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/loopback=]</td>
</tr>
<tr>
<td>`10.0.0.0/8`</td>
<td>Private Use</td>
<td>[[RFC1918]]</td>
<td>[=IP address space/local=]</td>
</tr>
<tr>
<td>`100.64.0.0/10`</td>
<td>Carrier-Grade NAT</td>
<td>[[RFC6598]]</td>
<td>[=IP address space/local=]</td>
</tr>
<tr>
<td>`172.16.0.0/12`</td>
<td>Private Use</td>
<td>[[RFC1918]]</td>
<td>[=IP address space/local=]</td>
</tr>
<tr>
<td>`192.168.0.0/16`</td>
<td>Private Use</td>
<td>[[RFC1918]]</td>
<td>[=IP address space/local=]</td>
</tr>
<tr>
<td>`198.18.0.0/15`</td>
<td>Benchmarking</td>
<td>[[RFC2544]]</td>
<td>[=IP address space/loopback=]</td>
</tr>
<tr>
<td>`169.254.0.0/16`</td>
<td>Link Local</td>
<td>[[RFC3927]]</td>
<td>[=IP address space/local=]</td>
</tr>
<tr>
<td>`::1/128`</td>
<td>IPv6 Loopback</td>
<td>[[RFC4291]]</td>
<td>[=IP address space/loopback=]</td>
</tr>
<tr>
<td>`fc00::/7`</td>
<td>Unique Local</td>
<td>[[RFC4193]]</td>
<td>[=IP address space/local=]</td>
</tr>
<tr>
<td>`fe80::/10`</td>
<td>Link-Local Unicast</td>
<td>[[RFC4291]]</td>
<td>[=IP address space/local=]</td>
</tr>
<tr>
<td>`fec0::/10`</td>
<td>Site-Local Unicast</td>
<td>[[RFC3513 obsolete]]</td>
<td>[=IP address space/local=]</td>
</tr>
<tr>
<td>`0.0.0.0/32`</td>
<td>IPv4 null IP address</td>
<td>[[RFC1884 obsolete]]</td>
<td>[=IP address space/loopback=]</td>
</tr>
<tr>
<td>`0.0.0.0/8`</td>
<td>IPv4 null IP addresses</td>
<td>[[RFC1884 obsolete]]</td>
<td>[=IP address space/local=]</td>
</tr>
<tr>
<td>`::/128`</td>
<td>IPv6 unspecified address</td>
<td>[[RFC1884 obsolete]]</td>
<td>[=IP address space/loopback=]</td>
</tr>
<tr>
<td>`2001:db8::/32`</td>
<td>IPv6 documentation addresses</td>
<td>[[RFC3849]]</td>
<td>[=IP address space/local=]</td>
</tr>
<tr>
<td>`3fff::/20`</td>
<td>IPv6 documentation addresses</td>
<td>[[RFC9637]]</td>
<td>[=IP address space/local=]</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/local=].
Note: Link-local IP addresses such as `169.254.0.0/16` are considered
[=IP address space/local=], 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/loopback=] instead.
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 <WICG/private-network-access#50>.
Note: [[PRIVATE-NETWORK-ACCESS obsolete]] used the address spaces public, private, and
local. This specification renames the address spaces to public, local, and
loopback, respectively.
Note: [[FETCH]] plans to specify that requests to the null IP address will be
treated as a network error. We map both the IPv4 null IP address space and the
IPv6 unspecified address to [=IP address space/loopback=] as some systems can
still route them back to the local host machine (and `0.0.0.0/8` can refer to
"specified hosts on this network", so is mapped to [=IP address space/local=])
per [[RFC5735 obsolete]]. Once the changes to Fetch have been made and adopted, we will
no longer need to handle them in this specification.
## Local Network Request ## {#local-network-request-section}
A [=request=] (|request|) is a <dfn export>local 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 two 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 [=local address=].
* false negatives: a client connected to two different
[=IP address space/local=] 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.
Requests originating from the [=loopback address=] should not be considered
[=local network requests=], and should not be subject to local network access
checks, since any software running on the user’s device is already in the most
privileged vantage point on the user’s network.
ISSUE: The definition of [=local 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 local network from
attacking other servers, including servers on `localhost`. See
<WICG/private-network-access#39>.
NOTE: Currently, Chromium only implements Local Network Access restrictions for
[=IP address space/public=] to [=IP address space/local=] or [=IP address
space/loopback=] requests, and does not enforce the permission for cross-origin
[=IP address space/local=] requests. This restriction can be shipped as an
incremental improvement in future implementations.
NOTE: Because local names and addresses are not meaningful outside the bounds of
the network, implementers might want to use a different permission prompt for the
cross-origin [=IP address space/local=] case than for the [=IP address
space/public=] to [=IP address space/local=] case. Additionally, implementers
might want to scope these permission grants to the specific network or to the
current browsing session only.
NOTE: Some [=local network requests=] are more challenging to secure than others.
See [[#rollout-difficulties]] for more details.
## Local Network Request Permission Prompt ## {#permission-prompt}
A local network access permission prompt is introduced to allow for users to
approve of [=local network requests=] from public websites to local network servers.
When a [=local network request=] is detected, a prompt is shown to the user asking
for permission to access the local network. If the user decides to grant the
permission, then the fetch continues. If not, it fails.
The exact scope of the permission is implementation-defined. The permission may
be as coarse-grained as allowing a specific origin to send [=local network
requests=] to any endpoint on the local network, or may be more fine-grained to
only allow specific origins to communicate with specific endpoints on the local
network. A user agent may persist this decision to reduce permission fatigue.
## Secure Context Restriction ## {#secure-context-restriction}
The capability to make local network requests is a [=powerful feature=]
and must only be allowed from [=secure contexts=].
ISSUE: To be able to apply LNA checks to all cross-origin [=IP address
space/local=] requests in the future (see Issue above), Chromium plans to
exempt local servers that likely cannot currently get publicly trusted HTTPS
certificates from this requirement (e.g., servers on `.local` and private IP
literals). See [[#rollout-difficulties]] for more discussion, and also see
<WICG/private-network-access#96>.
## Mixed Content ## {#mixed-content-lna}
Many local network servers do not run HTTPS, as it has proven difficult (and
sometimes even impossible) to migrate local network servers away from HTTP.
This is problematic as the [=secure context=] restriction, combined with mixed
content checks, would block many [=local network requests=] even if the user would
give permission for the request to occur.
One solution to this problem is to bypass mixed content checks in situations
where the request is known to be a [=local network request=]. This is known in a
few situations:
* When the hostname of the request target is an IP literal identified as [=IP
address space/local=] in the <a
href="#non-public-ip-address-blocks">Non-public IP address blocks"
table</a> (e.g., an [[RFC1918]] IP literal)
* When the hostname of the request is a .local domain (RFC 6762)
There may be situations in which neither of the above situations is true, and
yet the site wants to identify a request as being a [=local network request=]. This
can be mitigated by adding a new parameter to the `fetch()` options bag:
<pre highlight="js">
fetch("http://router.local/ping", {
targetAddressSpace: "local",
});
</pre>
This instructs the browser to allow the fetch to bypass mixed-content checks
even though the scheme is non-secure and potentially 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 resolved remote IP address does not belong to the IP address space
specified as the targetAddressSpace option value, then the request will fail.
If it does belong, then the permission can be checked to allow or fail the
request.
## Permissions ## {#permissions}
This document defines the following [=default powerful features=] which are
[=policy-controlled features=] with a [=policy-controlled feature/default allowlist=]
of [=default allowlist/'self'=]:
- <dfn export permission>"local-network"</dfn>, which controls the ability to make
[=local network requests=] to a [=local address=].
- <dfn export permission>"loopback-network"</dfn>, which controls the ability make
[=local network requests=] to a [=loopback address=].
NOTE: Previously, this was specified as a single [=default powerful feature=] that covered
both local and loopback cases, <dfn export permission>"local-network-access"</dfn>.
Chromium still supports this as an alias for the new fine-grained permission names, and for
use as a [=policy-controlled feature=] name.
# Integrations # {#integrations}
*This section is non-normative.*
This document proposes a number of modifications to other specifications in order
to implement the protections outlined above. These integrations are outlined here
for clarity, but the external documents are the normative references.
## Integration with Fetch ## {#integration-with-fetch}
This document proposes a few changes to [[FETCH]], with the following implication:
[=local network requests=] are only allowed if their
[=request/client=] is a [=secure context=]
**and** permission is granted by the user. 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 local 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.
ISSUE: Chromium only applies LNA restrictions to iframe navigations currently.
It may be worth expanding this to include main-frame navigations (especially
popup windows which can be controlled by their opener).
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. Local 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.
### Fetching ### {#fetching}
NOTE: Connection management in [[FETCH]] is intentionally a bit vague, so some of
the below is explained via notes to implementers. This section lays out that
implementers perform local network access checks after obtaining the connection
but before continuing to perform the fetch using that connection.
ISSUE: To address <WICG/local-network-access#103> we will need to instead integrate
into Step 4.6 of [=obtain a connection=] to perform checks after resolving the origin but before the actual connection is created.
Update [[FETCH]] as follows:
1. [=Connection=] objects are given a new <dfn export for="connection">IP
address</dfn> property, whose value is null or an [=IP address=], initially null.
1. Add a new step to the [=create a connection=] algorithm immediately after
establishing the new connection (between Steps 2 and 3).
1. If |host| is an [=IP address=], then set |connection|'s [=connection/IP address=]
to |host|.
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</dfn> property, whose value is null
or an [=IP address=], initially null.
1. Define a new <dfn export>Local Network Access check</dfn> algorithm.
Given a [=request=] |request| and an [=IP address=] |address|:
NOTE: This algorithm takes an [=IP address=] so that it can be called from
either [=HTTP-network fetch=] (where we have a [=connection=]) or
[=HTTP-network-or-cache fetch=] (where we have a stored [=response=] with
a saved [=IP address=]).
This intentionally re-computes the IP address space each time as the mapping
can be dynamically updated by the user agent.
1. Let |addressSpace| be the result of running the [=determine the IP
address space=] algorithm on |address|.
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 LNA
checks do not apply to |request|. Users of the [=fetch=] algorithm
have to 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:
1. [=Assert=]: |request|'s [=request/target IP address space=] is not
[=IP address space/public=].
1. If |addressSpace| is not equal to
then |request|'s [=request/target IP address space=], then return
a [=network error=].
1. Return null.
1. If |addressSpace| is [=IP address space/less public=] than |request|'s
[=request/policy container=]'s [=policy container/IP address space=]:
1. Let |error| be a [=network error=].
2. If |request|'s [=request/client=] is not a [=secure context=]
(including if it is null), then return |error|.
3. Set |error|'s [=response/IP address=] property to |address|.
4. Let |permissionName| be "local-network" if |addressSpace|
is [=IP address space/local=], or "loopback-network" if
|addressSpace| is [=IP address space/loopback=].
5. Let |settingsObject| be |request|'s [=request/client=].
6. Let |global| be |settingsObject|'s [=environment settings
object/global object=].
7. Let |document| be |global|'s [=global object/associated
Document=].
8. If |document| is null, then return |error|.
NOTE: This step will cause local network requests from Service
Workers to fail, as Service Workers do not always have an
associated Document. Future versions of this specification need
to define how to handle Workers, particularly since Permissions
Policy is not yet supported in Workers. See
[w3c/webappsec-permissions-policy#207](https://github.com/w3c/webappsec-permissions-policy/issues/207).
ISSUE: Define local network access behavior for Service Workers.
9. If |document| is not [=allowed to use=]
|permissionName|, then return |error|.
10. Let |permissionState| be the result of [=getting the current
permission state=] given |permissionName| and |global|.
11. If |permissionState| is [=permission/denied=], then return
|error|.
12. If |permissionState| is [=permission/granted=], then return
null.
13. [=Prompt the user to choose=] whether to grant
|permissionName| for |global|:
1. If the user grants permission, then return null.
2. If the user denies permission, then return |error|.
1. Return null.
1. The [$fetch$] algorithm is amended to add 2 new steps right after |request|’s
[=request/policy container=] is set:
1. If |request|’s [=request/target IP address space=] is null:
1. If |request|’s [=request/URL=]’s [=url/host=] *host* is an [=IP address=]
and the result of running the [=determine the IP address space=] algorithm
on *host* is [=IP address space/local=], then set *request*’s
[=request/target IP address space=] to [=IP address space/local=].
2. If |request|’s [=request/URL=]’s [=url/host=]’s [=host/public suffix=] is
`"local"`, then set |request|’s [=request/target IP address space=] to
[=IP address space/local=].
NOTE: We could also set the target IP address space to
[=IP address space/local=] if the request’s URL’s host is “localhost” or
“127.0.0.1” (because of [[LET-LOCALHOST-BE-LOCALHOST]]), but we do not need
special handling for the loopback case as it is already considered to be
potentially trustworthy and won’t trigger mixed content checks.
NOTE: We don’t set the target IP address space here if it was
already non-null in order to prefer the explicit
targetAddressSpace if set by the fetch() API.
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=] to
|connection|'s [=connection/IP address=].
1. Let |localNetworkAccessCheckResult| be the result of running
[=Local Network Access check=] for <var ignore>fetchParams</var>'
[=request=] and |connection|'s [=connection/IP address=].
1. If |localNetworkAccessCheckResult| is a [=network error=], then return
|localNetworkAccessCheckResult|.
1. The [=HTTP-network-or-cache fetch=] algorithm is amended to add a new step
right after Step 9:
1. If |response| is not null:
1. Let |localNetworkAccessCheckResult| be the result of running
[=Local Network Access check=] for |request| and |response|'s
[=response/IP address=].
2. If |localNetworkAccessCheckResult| is a [=network error=], then return |localNetworkAccessCheckResult|.
NOTE: The requirement that local network requests be made from secure contexts
means that any insecure request will be blocked as mixed content unless we can
know ahead of time that the request can be considered a local network
request. By setting the target IP address space property (see Step 6i and 6ii
above), we only need to make a small change to Mixed Content -- see
[[#integration-with-mixed-content]].
### Fetch API ### {#fetch-api}
The Fetch API needs to be adjusted as well.
* Append an optional [=map/entry=] to {{RequestInfo}}, whose [=map/key=] is
<dfn export>targetAddressSpace</dfn>, and [=map/value=] is a
{{IPAddressSpace}}.
<pre class="idl">
partial dictionary RequestInit {
IPAddressSpace targetAddressSpace;
};
</pre>
* Define a new targetAddressSpace representing the
above in [=request=].
<pre class="idl">
partial interface Request {
readonly attribute IPAddressSpace targetAddressSpace;
};
</pre>
* The <a constructor for=Request lt="Request(input, init)"><code>new
Request(<var ignore=''>input</var>, |init|)</code></a> is
appended with the following step right before setting [=this=]'s [=request=]
to |request|:
1. If |init|["{{RequestInit/targetAddressSpace}}"] [=map/exists=], then
switch on |init|["{{RequestInit/targetAddressSpace}}"]:
<dl class=switch>
<dt>public
<dd>Do nothing.
<dt>local
<dd>Set |request|'s targetAddressSpace to [=IP address
space/local=].
</dl>
## Integration with Mixed Content ## {#integration-with-mixed-content}
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/local=].
The "[Upgrade request to an a priori authenticated URL as mixed content, if
appropriate](https://w3c.github.io/webappsec-mixed-content/level2.html#upgrade-algorithm)"
algorithm is amended to add the following condition as an exception from
upgrading in step 1:
6. |request|’s[=request/target IP address space=] is [=IP address space/local=]
## Integration with WebSockets ## {#integration-with-websockets}
WebSockets connections should be subject to the same local network access
permission requirements. [$WebSocket opening handshake$] directly applies
[$fetch$] in step 11, and so no modification to the WebSocket specification is
required beyond the fetch change proposed above.
One minor difference between the Fetch API and the WebSockets API is that
WebSockets does not have an equivalent to fetch's `RequestInit`, and so there
is no place to put in a `targetAddressSpace` option to bypass mixed content
checks for ws:// urls.
## Integration with WebTransport ## {#integration-with-webtransport}
WebTransport connections should be subject to the same local network access
permission requirements.
No modification to the WebTransport spec will be required. In the WebTransport
spec, [obtaining a WebTransport
connection](https://www.w3.org/TR/webtransport/#obtain-a-webtransport-connection)
references the Fetch spec for obtaining a connection in step 6, which is being
modified in [[#fetching]]
## Integration with HTML ## {#integration-with-html}
To support the checks in [[FETCH]], user agents must remember the source IP
address space of contexts in which network requests are made. To this effect,
the [[HTML]] specification is patched as follows:
1. A new <dfn export for="policy container">IP address space </dfn> property
is added to the [=/policy container=] [=struct=].
1. It is initially public.
2. An additional step is added to the [=clone a policy container=] algorithm:
1. Set <var ignore>clone</var>'s [=policy container/IP address space=] to the
<var ignore>policyContainer</var>'s [=policy container/IP address space=].
3. An additional step is added to the [=create a policy container from a fetch response=]
algorithm:
1. Set <var ignore>result</var>'s [=policy container/IP address space=] the result of running the [=determine the IP address space=] algorithm on <var ignore>response</var>'s [=response/IP address=].
<div class="example" id="example-policy-container">
Assuming that `example.com` resolves to a [=public address=] (say,
`123.123.123.123`), then the {{Document}} created when navigating to
`https://example.com/document.html` will have its
[=Document/policy container=]'s [=policy container/IP address space=]
property set to [=IP address space/public=].
If this {{Document}} then embeds an `about:srcdoc` iframe, then the child
frame's {{Document}} will have its [=Document/policy container=]'s
[=policy container/IP address space=] property set to
[=IP address space/public=].
If, on the other hand, `example.com` resolved to a [=local address=]
(say, `127.0.0.1`), then the {{Document}} created when navigating to
`https://example.com/document.html` will have its
[=Document/policy container=]'s [=policy container/IP address space=]
property set to [=IP address space/local=].
</div>
TODO: Also update the reference to Private Network Access in [[html#coep]].
## Integration with Workers ## {#integration-with-workers}
*This section is non-normative.*
Given that {{WorkerGlobalScope}} already has a
[=WorkerGlobalScope/policy container=] field populated using the [=create a
policy container from a fetch response=] algorithm, the avove integrations
with Fetch and HTML apply just as well to worker contexts as to documents.
<div class="example" id="example-worker-policy">
Assuming that `example.com` resolves to a [=public address=] (say,
`123.123.123.123`), then a {{WorkerGlobalScope}} created by fetching a
script from `https://example.com/worker.js` will have its
[=WorkerGlobalScope/policy container=]'s
[=policy container/IP address space=] property set to
[=IP address space/public=].
Any fetch [=request=] initiated by this worker that [=obtains a connection=]
to an IP address in the [=IP address space/local=] or
[=IP address space/loopback=] [=address spaces=] would then be a
[=local network request=].
</div>
ISSUE: Chromium’s implementation currently applies the LNA permission for
service worker-initiated fetches based on the worker’s script origin (since
there may not be an active document around when the service worker is
executing). It may be better for this to be based on the partitioned storage
key of the worker, and it would also be good if permissions policy supported
service workers.
ISSUE: The [Service
Worker](https://w3c.github.io/ServiceWorker/#dfn-service-worker) [soft
update](https://w3c.github.io/ServiceWorker/#soft-update) algorithm
unfortunately sets a [request
client](https://fetch.spec.whatwg.org/#concept-request-client) of "null" when
[fetching](https://fetch.spec.whatwg.org/#concept-fetch) an updated script.
This causes all sorts of issues, and interferes with the local network access
check algorithm laid out above. Indeed, there is no [request
client](https://fetch.spec.whatwg.org/#concept-request-client) from which to
copy the [policy
container](https://fetch.spec.whatwg.org/#concept-request-policy-container)
during [fetch](https://fetch.spec.whatwg.org/#concept-fetch). See
<WICG/private-network-access#83>.
# Implementation considerations # {#implementation-considerations}
## File URLs ## {#file-urls}
It isn’t entirely clear how file URLs fit into the public/local scheme outlined
above. It would be nice to prevent folks from harming themselves by opening a
malicious HTML file locally, on the one hand, but on the other, code running
locally is somewhat outside of any coherent threat model.
For the moment, let’s err on the side of treating file URLs as local, as they
seem to be just as much a part of the local system as anything else on a
loopback address.
ISSUE: Re-evaluate this after implementation experience.
## Proxies ## {#proxies}
In the current implementation of this specification in Chromium, proxies
influence the address space of resources they proxy. Specifically, resources
fetched via proxies are considered to have been fetched from the proxy’s IP
address itself.
If a [=Document=] served by foo.example on a public address is fetched by the
user agent via a proxy on a local address, then the [=Document=]'s [=policy
container=]'s [=IP address space=] is set to local.
The [=Document=] will in turn be allowed to make requests to other local
addresses accessible to the browser.
This can allow a website to learn that it was proxied by observing that it is
allowed to make requests to local addresses, which is a privacy information
leak. While this requires correctly guessing the URL of a resource on the local
network, a single correct guess is sufficient.
This is expected to be relatively rare and not warrant more mitigations. After
all, in the status quo all websites can make requests to all IP addresses with
no restrictions whatsoever.
It would be interesting to explore a mechanism by which proxies could tell the
browser "please treat this resource as public/local anyway", thereby passing
on some information about the IP address behind the proxy.
## HTTP Cache ## {#http-cache}
Responses from the HTTP cache are subject to the Local Network Access
check, as specified in [[#integration-with-fetch]] above.
Below are some additional details about how to how those checks work in practice
(with some examples) depending on which kind of resource is loaded from cache.
### Main resources ### {#http-cache-main-resources}
A [=document=] constructed from a cached [=response=] remembers the IP address
from which the [=response=] was initially loaded. The IP address space of the
[=document=] is derived anew from the IP address.
In the common case, this entails that the [=document=]'s [=policy container=]'s
IP address space is restored unmodified. However in the event that the user
agent’s configuration has changed, the derived IP address space might be
different.
<div class="example" id="example-resource-cache-policy">
The user agent navigates to http://foo.example/, loads the main resource from
1.2.3.4, caches it, then sets the resulting [=document=]'s [=policy container=]'s
IP address space to public.
The user agent then restarts, and a new configuration is applied specifying
that 1.2.3.4 <span class="allow-2119">should</span> be classified as a local address instead.
The user agent navigates to http://foo.example/ once more and loads the main
resource from the HTTP cache. The resulting [=document=]'s [=policy container=]'s
IP address space is now set to local.