Private Network Access

Draft Community Group Report,

This version:
https://wicg.github.io/private-network-access/
Issue Tracking:
GitHub
Inline In Spec
Editor:
(Google)
Former Editor:
(Google)

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.

Status of this document

This specification was published by the Web Platform Incubator Community Group. It is not a W3C Standard nor is it on the W3C Standards Track. Please note that under the W3C Community Contributor License Agreement (CLA) there is a limited opt-out and other conditions apply. Learn more about W3C Community and Business Groups.

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:

1.2. Examples

1.2.1. Secure by Default

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 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:admin@router.local/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 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 Access-Control-Allow-Private-Network 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.

1.2.2. Opting-In

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 CORS-preflight request.

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
...
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 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

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 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:

  1. local: contains the local host only. In other words, addresses whose target differs for every device.

  2. private: contains addresses that have meaning only within the current network. In other words, addresses whose target differs based on network position.

  3. 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:

  1. A local address is an IP address whose IP address space is local.

  2. A private address is an IP address whose IP address space is private.

  3. 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:

  1. lhs is local and rhs is either private or public.

  2. lhs is private and rhs is public.

To determine the IP address space 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.

  2. For each row in the Non-public IP address blocks" table:

    1. If address belongs to row’s address block, return row’s address space.

  3. Return public.

Non-public IP address blocks
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:

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):

  1. 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:
  1. 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:

  1. Connection objects are given a new IP address space property, initially null. This applies to WebSocket connections too.

  2. 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 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]

  3. Request objects are given a new target IP address space property, initially null.

  4. Response objects are given a new IP address space property, whose value is an IP address space, initially null.

  5. Define a new Private Network Access check algorithm. Given a request request and a connection connection:

    1. 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.

    2. 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.

    3. If request’s target IP address space is not null, then:

      1. Assert: request’s target IP address space is not public.

      2. If connection’s IP address space is not equal to then request’s target IP address space, then return a network error.

      3. Return null.

    4. If connection’s IP address space is less public than request’s policy container's IP address space, then:

      1. Let error be a network error.

      2. If request’s client is not a secure context (including if it is null), then return error.

      3. Set error’s IP address space property to connection’s IP address space.

      4. Return error.

    5. Return null.

  6. The fetch algorithm is amended to add the following step immediately after request’s policy container is set:

    1. If request’s target IP address space is public, then return a network error.

  7. The HTTP-network fetch algorithm is amended to add 3 new steps right after checking that the newly-obtained connection is not failure:

    1. Set response’s IP address space to connection’s IP address space.

    2. Let privateNetworkAccessCheckResult be the result of running Private Network Access check for fetchParamsrequest and connection.

    3. If privateNetworkAccessCheckResult is a network error, return privateNetworkAccessCheckResult.

  8. Define a new algorithm to determine the preflight mode, given a request request and a boolean makeCORSPreflight:

    1. If makeCORSPreflight is true and one of these conditions is true:

      Then:

      1. If request’s target IP address space is not null, then return "cors+pna".

      2. Otherwise, return "cors".

    2. If request’s target IP address space is not null, then return "pna".

    3. Otherwise, return "none".

  9. 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:

    1. Let preflightMode be the result of invoking determine the preflight mode given request and makeCORSPreflight.

    2. Replace the entire condition "If makeCORSPreflight is true and ..., Then:" with:

      1. If preflightMode is not "none", then:

    3. Replace "running CORS-preflight fetch given request" with "running CORS-preflight fetch given request and preflightMode"

    4. Immediately after running CORS-preflight fetch:

      1. If preflightResponse is a network error:

        1. If preflightResponse’s IP address space is null, return preflightResponse.

        2. Set request’s target IP address space to preflightResponse’s IP address space.

        3. Return the result of running HTTP-no-service-worker fetch given fetchParams.

    5. Immediately after running HTTP-network-or-cache fetch:

      1. If response is a network error and response’s IP address space is non-null, then:

        1. Set request’s target IP address space to preflightResponse’s IP address space.

        2. 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.

  10. 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 `Access-Control-Request-Headers` to preflight’s header list if preflightMode is true.

    2. Immediately before running HTTP-network-or-cache fetch:

      1. If request’s target IP address space is not null, then:

        1. Set "Access-Control-Request-Private-Network" to "true" in preflight’s header list.

    3. Immediately after the CORS check:

      1. If preflightMode is "pna" or "cors+pna",

        1. Assert: request’s target IP address space is not null.

        2. Let allow be the result of extracting header list values given "Access-Control-Allow-Private-Network" and response’s header list.

        3. If allow is not "true", return a network error.

        4. Let requestWithoutTargetIpAddressSpace be a copy of request but set its target IP address space to be null.

        5. If should fetching requestWithoutTargetIpAddressSpace be blocked as mixed content returns allowed, return null.

        6. If Private-Network-Access-ID or Private-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.

        7. Let targetId be the result of extracting header list values given "Private-Network-Access-ID" and response’s header list.

        8. if targetId is not a string of 6 hexadecimal bytes separated by colons, return a network error.

        9. Let targetName be the result of extracting header list values given "Private-Network-Access-Name" and response’s header list.

        10. if targetName does not match the [ECMAScript] regexp /^[a-z0-9_-.]+$/ or has more than 248 UTF-8 code units, return a network error.

        11. Let state be the result of requesting permission to use the following descriptor:

          {
            name: "private-network-access",
            id: targetId,
          }
          
        12. If state is "denied", return a network error.

        13. Return null.

  11. 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:

    1. A new IP address space property (null or an IP address space) is added to each cache entry.

    2. This new property is initialized by the create a new cache entry algorithm from request’s target IP address space.

    3. This new property is checked by the cache entry match algorithm:

      1. 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.

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:

  1. A new IP address space 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 clone’s IP address space to policyContainer’s IP address space.

  3. An additional step is added to the create a policy container from a fetch response algorithm:

    1. Set result’s IP address space to response’s IP address space.

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 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.

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 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.

If a 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.

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 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:

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.

The user agent navigates to 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.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[CSP3]
Mike West; Antonio Sartori. Content Security Policy Level 3. URL: https://w3c.github.io/webappsec-csp/
[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[FETCH]
Anne van Kesteren. Fetch Standard. Living Standard. URL: https://fetch.spec.whatwg.org/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[MIXED-CONTENT]
Emily Stark; Mike West; Carlos IbarraLopez. Mixed Content. URL: https://w3c.github.io/webappsec-mixed-content/
[PERMISSIONS]
Marcos Caceres; Mike Taylor. Permissions. URL: https://w3c.github.io/permissions/
[RFC7762]
M. West. Initial Assignment for the Content Security Policy Directives Registry. January 2016. Informational. URL: https://www.rfc-editor.org/rfc/rfc7762
[SECURE-CONTEXTS]
Mike West. Secure Contexts. URL: https://w3c.github.io/webappsec-secure-contexts/
[SERVICE-WORKERS]
Jake Archibald; Marijn Kruisselbrink. Service Workers. URL: https://w3c.github.io/ServiceWorker/
[URL]
Anne van Kesteren. URL Standard. Living Standard. URL: https://url.spec.whatwg.org/
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL Standard. Living Standard. URL: https://webidl.spec.whatwg.org/
[WEBSOCKETS]
Adam Rice. WebSockets Standard. Living Standard. URL: https://websockets.spec.whatwg.org/

Informative References

[AVASTIUM]
Avast: A web-accessible RPC endpoint can launch 'SafeZone' (also called Avastium), a Chromium fork with critical security checks removed.. URL: https://bugs.chromium.org/p/project-zero/issues/detail?id=679
[CSRF-EXPLOIT-KIT]
Kafeine. An Exploit Kit dedicated to CSRF Pharming. URL: http://malware.dontneedcoffee.com/2015/05/an-exploit-kit-dedicated-to-csrf.html
[DRIVE-BY-PHARMING]
Sid Stamm; Zulfikar Ramzan; Markus Jakobsson. Drive-By Pharming. URL: https://link.springer.com/chapter/10.1007/978-3-540-77048-0_38
[IPV4-REGISTRY]
IANA IPv4 Special-Purpose Address Registry. URL: https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
[IPV6-REGISTRY]
IANA IPv6 Special-Purpose Address Registry. URL: https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
B. Carpenter; S. Cheshire; R. Hinden. Representing IPv6 Zone Identifiers in Address Literals and Uniform Resource Identifiers. 12 April 2023. Internet-Draft. URL: https://www.ietf.org/archive/id/draft-ietf-6man-rfc6874bis-07.html
[MIXED-CONTENT-2]
Emily Stark; Mike West; Carlos Ibarra Lopez. Mixed Content Level 2. 14 October 2020. W3C First Public Working Draft. URL: https://w3c.github.io/webappsec-mixed-content/level2.html
[PLEX]
Filippo Valsorda. How Plex is doing HTTPS for all its users. URL: https://blog.filippo.io/how-plex-is-doing-https-for-all-its-users/
[RFC1122]
R. Braden, Ed.. Requirements for Internet Hosts - Communication Layers. October 1989. Internet Standard. URL: https://www.rfc-editor.org/rfc/rfc1122
[RFC1918]
Y. Rekhter; et al. Address Allocation for Private Internets. February 1996. Best Current Practice. URL: https://www.rfc-editor.org/rfc/rfc1918
[RFC2544]
S. Bradner; J. McQuaid. Benchmarking Methodology for Network Interconnect Devices. March 1999. Informational. URL: https://www.rfc-editor.org/rfc/rfc2544
[RFC3927]
S. Cheshire; B. Aboba; E. Guttman. Dynamic Configuration of IPv4 Link-Local Addresses. May 2005. Proposed Standard. URL: https://www.rfc-editor.org/rfc/rfc3927
[RFC4193]
R. Hinden; B. Haberman. Unique Local IPv6 Unicast Addresses. October 2005. Proposed Standard. URL: https://www.rfc-editor.org/rfc/rfc4193
[RFC4291]
R. Hinden; S. Deering. IP Version 6 Addressing Architecture. February 2006. Draft Standard. URL: https://www.rfc-editor.org/rfc/rfc4291
[RFC6555]
D. Wing; A. Yourtchenko. Happy Eyeballs: Success with Dual-Stack Hosts. April 2012. Proposed Standard. URL: https://www.rfc-editor.org/rfc/rfc6555
[RFC6598]
J. Weil; et al. IANA-Reserved IPv4 Prefix for Shared Address Space. April 2012. Best Current Practice. URL: https://www.rfc-editor.org/rfc/rfc6598
[RFC6762]
S. Cheshire; M. Krochmal. Multicast DNS. February 2013. Proposed Standard. URL: https://www.rfc-editor.org/rfc/rfc6762
[RFC8305]
D. Schinazi; T. Pauly. Happy Eyeballs Version 2: Better Connectivity Using Concurrency. December 2017. Proposed Standard. URL: https://www.rfc-editor.org/rfc/rfc8305
[SECURE-LOCAL-COMMUNICATION]
Minutes from 'Secure communication with local network devices': TPAC, 2015. URL: http://www.w3.org/2015/10/28-local-minutes.html
[SOHO-PHARMING]
Team Cymru. SOHO Pharming. URL: https://331.cybersec.fun/TeamCymruSOHOPharming.pdf
[TREND-MICRO]
TrendMicro node.js HTTP server listening on localhost can execute commands. URL: https://bugs.chromium.org/p/project-zero/issues/detail?id=693

IDL Index

enum IPAddressSpace { "public", "private", "local" };

dictionary PrivateNetworkAccessPermissionDescriptor
    : PermissionDescriptor {
  DOMString id;
};

partial dictionary RequestInit {
  IPAddressSpace targetAddressSpace;
};

partial interface Request {
  readonly attribute IPAddressSpace targetAddressSpace;
};

Issues Index

Remove the special case for IPv4-mapped IPv6 addresses once access to these addresses is blocked entirely. [Issue #36]
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]
The remote endpoint concept is not specified in [FETCH] yet, hence this is still handwaving to some extent. [Issue #33]
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]
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]
Reevaluate this after implementation experience.
Specify and explain Chromium’s behavior here. [Issue #75]
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]
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]
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]