1. Introduction
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 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.
1.1. 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 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).
1.2. Examples
1.2.1. Secure by Default
http://admin:[email protected]/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 CORS-preflight request, which the router ignores. Let’s take a closer look:
Given https://csrf.attack/
that contains the following HTML:
<iframe href="https://admin:[email protected]/set_dns?server1=123.123.123.123"> </iframe>
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 private. Since csrf.attack
resolved to a public address, the request will trigger a CORS-preflight request:
OPTIONS /set_dns?... HTTP/1.1 Host: router.local Access-Control-Request-Method: GET Access-Control-Request-Private-Network: true ... Origin: https://csrf.attack
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 a50X
error. This will cause the preflight to fail, and the actualGET
will never be issued. -
If it does understand
OPTIONS
, it can neglect to include anAccess-Control-Allow-Private-Network
header in its response. This will cause the preflight to fail, and the actualGET
will never be issued. -
It can crash. Crashing is fairly safe, if inelegant.
1.2.2. Opting-In
When a website on the public internet makes a request to the device, the user agent determines that the requestor is public, and the router is 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:
HTTP/1.1 200 OK ... Access-Control-Allow-Origin: https://public.example.com Access-Control-Allow-Methods: GET Access-Control-Allow-Credentials: true Access-Control-Allow-Private-Network: true Content-Length: 0 ...
1.2.3. Navigation
https://go/
, and
its employees often email such links to each other. The email server is
hosted at a public address 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 CORS-preflight request, as it is a request from a public
address to a private address:
OPTIONS /short-links-are-short-after-shortening HTTP/1.1 Host: go Access-Control-Request-Method: GET Access-Control-Request-Private-Network: true ... Origin: https://mail.mega.corp
In order to ensure that employees can continue to navigate such links as expected, MegaCorp chooses to allow private network requests:
HTTP/1.1 200 OK ... Access-Control-Allow-Origin: https://mail.mega.corp Access-Control-Allow-Methods: GET Access-Control-Allow-Credentials: true Access-Control-Allow-Private-Network: true Content-Length: 0 ...
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 only 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:
// 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
The navigation will proceed normally, but mail.mega.corp
won’t be
considered CORS-same-origin with the response.
1.2.4. Mixed Content
When a website with a potentially trustworthy origin on the public internet requests data from the device, the user agent recognizes the requestor as public, and the device as 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:
fetch( "http://router.local/ping" , { targetAddressSpace: "private" , });
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:
HTTP/1.1 200 OK ... Access-Control-Allow-Origin: https://mail.mega.corp Access-Control-Allow-Methods: GET Access-Control-Allow-Credentials: true Access-Control-Allow-Private-Network: true Private-Network-Access-ID: 01:23:45:67:89:0A Private-Network-Access-Name: userA’s MegaCorp device Content-Length: 0 ...
A permission prompt will appear, displaying the ID and name from the device header. If the user grants permission, the request will proceed.
2. Framework
2.1. IP Address Space
Define IPAddressSpace
as follows:
enum {
IPAddressSpace ,
"public" ,
"private" };
"local"
Every IP address belongs to an IP address space, which can be one of three different values:
-
local: contains the local host only. In other words, addresses whose target differs for every device.
-
private: contains addresses that have meaning only within the current network. In other words, addresses whose target differs based on network position.
-
public: 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:
-
A local address is an IP address whose IP address space is local.
-
A private address is an IP address whose IP address space is private.
-
A public address is an IP address whose IP address space is public.
An IP address space lhs is less public than an IP address space rhs if any of the following conditions holds true:
To determine the IP address space of an IP address address, run the following steps:
-
If address belongs to the
::ffff:0:0/96
"IPv4-mapped Address" address block, then replace address with its embedded IPv4 address. -
For each row in the Non-public IP address blocks" table:
-
If address belongs to row’s address block, return row’s address space.
-
-
Return public.
Address block | Name | Reference | Address space |
---|---|---|---|
127.0.0.0/8
| IPv4 Loopback | [RFC1122] | local |
10.0.0.0/8
| Private Use | [RFC1918] | private |
100.64.0.0/10
| Carrier-Grade NAT | [RFC6598] | private |
172.16.0.0/12
| Private Use | [RFC1918] | private |
192.168.0.0/16
| Private Use | [RFC1918] | private |
198.18.0.0/15
| Benchmarking | [RFC2544] | local |
169.254.0.0/16
| Link Local | [RFC3927] | private |
::1/128
| IPv6 Loopback | [RFC4291] | local |
fc00::/7
| Unique Local | [RFC4193] | private |
fe80::/10
| Link-Local Unicast | [RFC4291] | private |
::ffff:0:0/96
| IPv4-mapped | [RFC4291] | see mapped IPv4 address |
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 public per the algorithm above, by instead configuring user agents to treat the intranet as private.
Note: Link-local IP addresses such as 169.254.0.0/16
are considered 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 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.
Remove the special case for IPv4-mapped IPv6 addresses once access to these addresses is blocked entirely. [Issue #36]
2.2. Private Network Request
A request (request) is a private network request if request’s current url's host
maps to an IP address
whose IP address space is less public than request’s policy container's 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 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.
The definition of private network requests could be expanded to
cover all cross-origin requests for which the current url's host
maps to an IP address whose IP address space is not 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. [Issue #39]
NOTE: Some private network requests are more challenging to secure than others. See § 4.4 Rollout difficulties for more details.
2.3. Additional CORS Headers
The Access-Control-Request-Private-Network
indicates that the request is a private network request.
The Access-Control-Allow-Private-Network
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 for
details.
The Private-Network-Access-Name
attempts to
provide users a human friendly name in the private network access
permission prompt.
The Private-Network-Access-ID
header is used
in the PrivateNetworkAccessPermissionDescriptor
to identify identical
devices across IP addresses.
2.4. The treat-as-public-address
Content Security Policy Directive
The treat-as-public-address 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:
directive-name = "treat-as-public-address" directive-value = ""
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 initialization algorithm is as follows. Given
an environment settings object (context), a Response
(response), and
a policy
(policy):
-
Set context’s policy container's IP address space to public if policy’s disposition is "
enforce
".
2.5. Feature Detection
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).
Documents should not behave differently or not based on whether the UA implements this specification or not - all documents should assume it does.
2.6. Permission Prompt
Following the discussions in [Issue#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 secure context restriction 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:
fetch( "http://router.local/ping" , { targetAddressSpace: "private" , });
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:
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>
For example:
Private-Network-Access-Name: "My Smart Toothbrush" Private-Network-Access-ID: "01:23:45:67:89:0A"
Private-Network-Access-ID
should be a 48-bit value presented as 6
hexadecimal bytes separated by colons. Private-Network-Access-Name
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.
3. Integrations
This section is non-normative.
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.
3.1. Secure context restriction
UAs must not allow non-secure 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 private resources presents risks which are mitigated by ensuring the integrity of the 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.
3.2. Integration with Permissions
This document defines a powerful feature identified by the name "private-network-access"
. It
overrides the following type:
- permission descriptor type
-
The permission descriptor type of the
"private-network-access"
feature is defined by the following WebIDL interface that inherits from the default permission descriptor type:dictionary
:PrivateNetworkAccessPermissionDescriptor PermissionDescriptor {DOMString
; };id
3.3. 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:-
request’s origin is not a potentially trustworthy origin, and request’s target IP address space is private or local.
3.4. Integration with Fetch
This document proposes a few changes to Fetch, with the following implication: private network requests are only allowed if their 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], [RFC8305]), these checks might pass or fail non-deterministically for hosts with multiple IP addresses that straddle IP address space boundaries.
3.4.1. CORS preflight
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 IP address space is not known until a connection is obtained in HTTP-network fetch, which is layered under CORS-preflight fetch.
3.4.2. Fetching
What follows is a sketch of a potential solution:
-
Connection objects are given a new IP address space property, initially null. This applies to WebSocket connections too.
-
A new step is added to the obtain a connection algorithm immediately before appending connection to the user agent’s connection pool:
-
Set connection’s IP address space to the result of running the determine the IP address space algorithm on the IP address of connection’s remote endpoint.
The remote endpoint concept is not specified in [FETCH] yet, hence this is still handwaving to some extent. [Issue #33]
-
-
Request objects are given a new target IP address space property, initially null.
-
Response objects are given a new IP address space property, whose value is an IP address space, initially null.
-
Define a new Private Network Access check algorithm. Given a request request and a connection connection:
-
If request’s origin is a potentially trustworthy origin and request’s current URL’s origin is same origin with request’s origin, then return null.
-
If request’s policy container is null, then return null.
NOTE: If request’s 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 client to an environment settings object with a non-null policy container and let fetch initialize request’s policy container accordingly, or to directly set request’s policy container to a non-null value.
-
If request’s target IP address space is not null, then:
-
Assert: request’s target IP address space is not public.
-
If connection’s IP address space is not equal to then request’s target IP address space, then return a network error.
-
Return null.
-
-
If connection’s IP address space is less public than request’s policy container's IP address space, then:
-
Let error be a network error.
-
If request’s client is not a secure context (including if it is null), then return error.
-
Set error’s IP address space property to connection’s IP address space.
-
Return error.
-
-
Return null.
-
-
The fetch algorithm is amended to add the following step immediately after request’s policy container is set:
-
If request’s target IP address space is public, then return a network error.
-
-
The HTTP-network fetch algorithm is amended to add 3 new steps right after checking that the newly-obtained connection is not failure:
-
Set response’s IP address space to connection’s IP address space.
-
Let privateNetworkAccessCheckResult be the result of running Private Network Access check for fetchParams’ request and connection.
-
If privateNetworkAccessCheckResult is a network error, return privateNetworkAccessCheckResult.
-
-
Define a new algorithm to determine the preflight mode, given a request request and a boolean makeCORSPreflight:
-
If makeCORSPreflight is true and one of these conditions is true:
-
There is no method cache entry match for request’s method using request, and either request’s method is not a CORS-safelisted method or request’s use-CORS-preflight flag is set.
-
There is at least one item in the CORS-unsafe request-header names with request’s header list for which there is no header-name cache entry match using request.
Then:
-
If request’s target IP address space is not null, then return "cors+pna".
-
Otherwise, return "cors".
-
-
If request’s target IP address space is not null, then return "pna".
-
Otherwise, return "none".
-
-
Define a new algorithm called HTTP-no-service-worker fetch 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:
-
Let preflightMode be the result of invoking determine the preflight mode given request and makeCORSPreflight.
-
Replace the entire condition "If makeCORSPreflight is true and ..., Then:" with:
-
If preflightMode is not "none", then:
-
-
Replace "running CORS-preflight fetch given request" with "running CORS-preflight fetch given request and preflightMode"
-
Immediately after running CORS-preflight fetch:
-
If preflightResponse is a network error:
-
If preflightResponse’s IP address space is null, return preflightResponse.
-
Set request’s target IP address space to preflightResponse’s IP address space.
-
Return the result of running HTTP-no-service-worker fetch given fetchParams.
-
-
-
Immediately after running HTTP-network-or-cache fetch:
-
If response is a network error and response’s IP address space is non-null, then:
-
Set request’s target IP address space to preflightResponse’s IP address space.
-
Return the result of running HTTP-no-service-worker fetch given fetchParams.
-
-
Note: Because request’s target IP address space is set to a non-null value when recursing, this recursion can go at most 1 level deep.
-
-
The CORS-preflight fetch algorithm is adjusted to take a new parameter preflightMode (default "cors"), and handle the new headers as follows:
-
Only append `
Accept
` and `Access-Control-Request-Headers
` to preflight’s header list if preflightMode is true. -
Immediately before running HTTP-network-or-cache fetch:
-
If request’s target IP address space is not null, then:
-
Set "
Access-Control-Request-Private-Network
" to "true
" in preflight’s header list.
-
-
-
Immediately after the CORS check:
-
If preflightMode is "pna" or "cors+pna",
-
Assert: request’s target IP address space is not null.
-
Let allow be the result of extracting header list values given "
Access-Control-Allow-Private-Network
" and response’s header list. -
If allow is not "
true
", return a network error. -
Let requestWithoutTargetIpAddressSpace be a copy of request but set its target IP address space to be null.
-
If should fetching requestWithoutTargetIpAddressSpace be blocked as mixed content returns allowed, return null.
-
If
Private-Network-Access-ID
orPrivate-Network-Access-Name
is null or empty, let targetId be request’s target IP address space. Store the permission as an ephemeral permission, then return null. -
Let targetId be the result of extracting header list values given "
Private-Network-Access-ID
" and response’s header list. -
if targetId is not a string of 6 hexadecimal bytes separated by colons, return a network error.
-
Let targetName be the result of extracting header list values given "
Private-Network-Access-Name
" and response’s header list. -
if targetName does not match the [ECMAScript] regexp /^[a-z0-9_-.]+$/ or has more than 248 UTF-8 code units, return a network error.
-
Let state be the result of requesting permission to use the following descriptor:
{ name: "private-network-access" , id: targetId, } -
If state is
"denied"
, return a network error. -
Return null.
-
-
-
-
Finally, to mitigate the impact of DNS rebinding attacks (see § 5.3 DNS Rebinding), the CORS-preflight cache is adjusted to take IP address space information into account:
-
A new IP address space property (null or an IP address space) is added to each cache entry.
-
This new property is initialized by the create a new cache entry algorithm from request’s target IP address space.
-
This new property is checked by the cache entry match algorithm:
-
entry’s IP address space is equal to request’s target IP address space.
-
-
3.4.3. Fetch API
The Fetch API needs to be adjusted as well.
-
Append an optional entry to
RequestInfo
, whose key is targetAddressSpace, and value is aIPAddressSpace
.partial dictionary RequestInit {IPAddressSpace
; };targetAddressSpace -
Define a new {=targetAddressSpace=} representing the above in request.
partial interface Request {readonly attribute IPAddressSpace
; };targetAddressSpace -
The
new Request(input, init)
is appended with the following step right before setting this's request to request:-
If init["
targetAddressSpace
"] exists, then switch on init["targetAddressSpace
"]:- public
- Do nothing.
- private
- Set request’s target IP address space to private.
- local
- Set request’s target IP address space to local.
-
3.4.4. Forbidden header names
A new entry is added to the list of forbidden request-header names: Access-Control-Request-Private-Network
.
The user agent should have full control over this header, just as it does over other CORS headers.
3.5. Integration with WebSockets
Preflight requests should probably be sent ahead of WebSocket
handshakes, given that WebSocket handshakes have roughly the same capabilities
as <img>
tags. This might require no additional work to specify given that
the establish a WebSocket connection depends on the Fetch algorithm. [Issue #14]
A previous version of this specification proposed simply adding the new headers (see § 2.3 Additional CORS Headers) to the WebSocket handshake. This would not be sufficient to fully guard against CSRF attacks, however.
3.6. 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:
-
A new IP address space property is added to the policy container struct.
-
It is initially public.
-
-
An additional step is added to the clone a policy container algorithm:
-
Set clone’s IP address space to policyContainer’s IP address space.
-
-
An additional step is added to the create a policy container from a fetch response algorithm:
-
Set result’s IP address space to response’s IP address space.
-
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 policy container's IP address space property set to public.
If this Document
then embeds an about:srcdoc
iframe, then the child
frame’s Document
will have its policy container's IP address space property set to 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 policy container's IP address space property set to local.
3.7. Workers
This section is non-normative.
Given that WorkerGlobalScope
already has a 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.
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 policy container's IP address space property set to public.
Any fetch request initiated by this worker that obtains a connection to an IP address in the private or local address spaces would then be a private network request.
The Service Worker soft update algorithm unfortunately sets a request client of "null"
when fetching an updated script. This causes all sorts of issues, and
interferes with the private network access check algorithm laid out above.
Indeed, there is no request client from which to copy the policy container during fetch. [Issue #83]
4. Implementation Considerations
4.1. Where do file
URLs fit?
It isn’t entirely clear how file
URLs fit into the public/private 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.
Reevaluate this after implementation experience.
4.2. 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.
Document
served by foo.example
on a public address is fetched
by the user agent via a proxy on a private address, then the Document
's policy container's IP address space is set to private.
The Document
will in turn be allowed to make requests to other private 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 private addresses, which is a privacy information leak. While this requires correctly guessing the URL of a resource on the private 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/private anyway", thereby passing on some information about the IP address behing the proxy. This might take the form of the CSP directive discussed above, with some minor modifications.
4.3. HTTP Cache
The current implementation of this specification in Chromium interacts with the HTTP cache in two noteworthy ways, depending on which kind of resource is loaded from cache.
4.3.1. Main resources
A document constructed from a cached response remembers the IP address whence 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.
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
should be classified as a private 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 private.
4.3.2. Subresources
Subresources loaded from the HTTP cache are subject to the Private Network Access check. This is not yet reflected in the algoritms above, since that check is only applied in HTTP-network fetch.
Specify and explain Chromium’s behavior here. [Issue #75]
See § 5.6 HTTP cache for a discussion of security implications.
4.4. Rollout difficulties
Private Network Access essentially deprecates direct access to the private network in favor of more secure user-agent-mediated alternatives. Web deprecations are hard. Chromium has encountered many stumbling blocks on the way to shipping parts of this specification.
In particular, shipping restrictions on fetches from non-secure contexts in the private IP address space to the local IP address space has proven particularly difficult, for a lower payoff. Indeed, exploiting such fetches requires attackers to already have a foothold in the private network, which substantially raises attack difficulty. As a result, Chromium exempted these fetches from restrictions temporarily, choosing to focus on fetches from the public IP address space.
5. Security and Privacy Considerations
5.1. User Mediation
The proposal in this document only ensures that the device consents to access from the public internet. Users agents MAY ensure that the user consents to such access as well, as it might be in their interests to deny such access, even though the device itself would allow it.
This mediation could be done via an explicit permission grant, via some sort of pairing ceremony a la PAKE, or any other clever interface which the user agent might devise.
5.2. Mixed Content
The CORS restrictions added by the proposal in this document do not obviate mixed content checks [MIXED-CONTENT-2]. Device consent obtained through a CORS preflight request is necessary but not sufficient.
Note: [MIXED-CONTENT-2] does not prevent secure contexts from fetching
resources from origins whose host is localhost
or an IP
address in the 127.0.0.0/8
or ::1/128
blocks. See also the definition of potentially trustworthy origins.
Developers who wish to fetch private or local resources (from hosts other than the above exceptions) from public pages MUST ensure that the connection is secure. This might involve a solution along the lines of [PLEX], where Web PKI certificates are issued to user-specific domain names that then resolve to private IP addresses which only make sense on the user’s private network.
Some consumer routers implement overly-aggressive protections against DNS rebinding attacks by simply blocking DNS responses that resolve to non-public IP addresses. This presents a stumbling block for solutions like [PLEX]. Workarounds are discussed in the linked issue. [Issue #23]
This problem space has been explored a few times already and seems worth revisiting at some point. One could imagine a pairing ceremony such as the one hinted at above, or one of the ideas floated in [SECURE-LOCAL-COMMUNICATION].
5.3. DNS Rebinding
The mitigation described here operates upon the IP address which the user agent actually connects to when loading a particular resource. This check MUST be performed for each new connection made, as DNS rebinding attacks may otherwise trick the user agent into revealing information it shouldn’t.
The modifications to the CORS-preflight cache are intended to mitigate this attack vector.
5.4. Scope of Mitigation
The proposal in this document merely mitigates attacks against private web services, it cannot fully solve them. For example, a router’s web-based administration interface must be designed and implemented to defend against CSRF on its own, and should not rely on a UA that behaves as specified in this document. The mitigation this document specifies is necessary given the reality of private web service implementation quality today, but vendors should not consider themselves absolved of responsibility, even if all UAs implement this mitigation.
5.5. Cross-network confusion
Most private networks cannot communicate with each other, yet they are all treated by this specification as belonging to the private IP address space. Going further, private addresses have meaning only on the private network where they are used. The same IP address might refer to entirely different devices in two different networks.
This opens the door to cross-network attacks:
-
A user connects to two different private networks: a home Wi-Fi network and a corporate VPN. Their smart fridge has been hacked. They open their smart fridge’s web interface, which then performs CSRF attacks against corporate websites accessible via the VPN.
-
A user connects to a malicious internet cafe Wi-Fi, which requires users to keep a captive portal page open. They close their laptop, go home, open up their laptop again. The captive portal page (either still open or reloaded from cache as the user agent restores its previous state) performs CSRF attacks against the user’s home devices.
-
A user connects to a malicious internet cafe Wi-Fi, whose captive portal website caches a malicious script from
http://router.example/popular-library.js
(the cafe network administrator operates a malicious DNS server) with a very long expiry. The user powers their computer off, goes home, boots up their computer again and visits their router’s administration interface athttp://router.example
, which embeds/popular-library.js
. The malicious script is loaded in the administration interface’s first-party context.
None of these attacks are novel - they are just examples of the limitations of this specification.
Potential mitigations would require noticing network changes and clearing state specific to the previous network. Doing so in a fully general manner is likely to be impossible short of clearing all state. Maybe a practical compromise can be reached. [Issue #28]
5.6. HTTP cache
5.6.1. Applying checks to subresources
The following is no longer accurate. Implementation experience revealed that integrating with the cache was useful even in protecting network resources against CSRF attacks. This section needs to be rewritten. [Issue #75]
Cached subresources are not currently protected by this specification, even though the HTTP cache remembers the source IP address which could be used in the Private Network Access check algorithm during HTTP-network-or-cache fetch.
While it may be a good idea to fix this apparent discrepancy, it is not directly relevant to the main goal of this specification: preventing CSRF attacks.
At most, a malicious public website might be able to determine whether a user has visited particular private websites in the past. This attack on the user’s privacy is no worse than the status quo.
In addition, due to HTTP cache partitioning, a subresource can only be loaded from cache by malicious attackers who manage to replicate the network partition key of the cache entry. One way an attacker could achieve this is by manipulating DNS (see also § 5.3 DNS Rebinding) in order to impersonate the top-level site that initially embedded the cached resource.
http://router.example
, which is served from 192.168.1.1
. The website embeds a logo from http://router.example/$BRAND-logo.png
, which is cached.
A malicious attacker then re-binds router.example
to an
attacker-controlled public IP address, and somehow tricks the user into
visiting http://router.example
again. The malicious website attempts to
embed the logo, and monitors whether the load is successful. If so, the
attacker has determined the brand of the user’s router.
5.6.2. HTTP cache poisoning
While this specification aims to protect private network servers from receiving requests from public websites, DNS rebinding can be used to carry out a similar attack through cache poisoning of unauthenticated resources.
Attackers masquerading as http://router.com
can cache a malicious script at http://router.com/totally-legit.js
. Later on, when the user navigates to http://router.com/
, the page might request the poisoned script and execute
attacker code in a less public IP address space.
This attack is partially mitigated by cache partitioning,
which makes it so that the attacker must navigate a top-level browsing context
to http://router.com/
before caching resources, which lacks subtlety. It is
also not specific to Private Network Access, rather being a symptom of
plaintext HTTP’s lack of authentication and integrity protection.
6. IANA Considerations
The Content Security Policy Directive registry should be updated with the following directives and references [RFC7762]:
treat-as-public-address
-
This document (see § 2.4 The treat-as-public-address Content Security Policy Directive)
7. Acknowledgements
Conversations with Ryan Sleevi, Chris Palmer, and Justin Schuh helped flesh out the contours of this proposal. Hopefully they won’t hate it too much. Mathias Karlsson has the dubious honor of being the straw that broke the camel’s back, and Brian Smith’s contributions to the resulting thread were useful, as always.