Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Private Network Access Permission Prompt Origin Trial Feedback / Questions #117

Open
DanielBaulig opened this issue Nov 2, 2023 · 12 comments

Comments

@DanielBaulig
Copy link

Please let me know if there is a better forum to provide this feedback and/or ask these questions.

I've been messing around with the Private Network Access Permission Prompt Origin Trial in Chrome Nightly and ran into some issues that I would like to flag and/or ask for some advice on how to resolve.

I put together a simple locally hosted nginx that simply responds to all requests with the Private-Network-Access CORS headers. In my very first attempt the response did not include the Private-Network-Access-ID or Private-Network-Access-Name headers on any requests, but the permission dialog still showed up and I was able to grant permission to connect to the local host. The fetch request that triggered the permission dialog completed successfully.

Since the specification -- as far as I understand -- says that a preflight request response without a valid targetId should result in a network error I was a little surprised that this worked at all. I believe that I did get a warning about the Private-Network-Access-ID being invalid in the console though.

I then continued to add the Private-Network-Access-ID and Private-Network-Access-Name headers to my nginx config to see how the permission dialog changes when they are present. However, it is unclear to me how I can revoke the permission again. I went to the site permission settings and couldn't find anything there. I proceeded to reset all permissions and delete all site data, but I cannot get the permission dialog to re-appear. Not only is this obviously an issue for development and testing, but it also seems like a major oversight if the user cannot revoke their permission at a later point anymore. Please point me in the right direction if the permission is currently already revocable.

Lastly, I was under the impression that the { targetAddressSpace: 'private' } options bag would only have to be provided on the first network request to the private origin server for the duration of the document lifetime. I was under the impression that this mechanism would also allow for usage of other non-fetch request sources like HTML elements or XHR. At least for me currently that does not seem to be the case. I have to provide the { targetAddressSpace: 'private' } options bag on each fetch request for them to be succesfull. In addition I don't seem to be able to use other mechanisms to trigger successful network requests to the origin host. In my particular use-case I am intending on using Server Sent Events / EventSource and the requests being sent by EventSource fail due to the mixed content policy, even after having sent a fetch positively acknowledged fetch request with { targetAddressSpace: 'private' } options bag.

I recognize that I may have misunderstood how this is supposed to work or that the lack of a valid Private-Network-Access-ID on the first request triggering the permission dialog might have caused an unintended change in behavior. It does not appear that having added these headers retroactively changed anything. Note, that for testing purposes I have set a random string as the Private-Network-Access-ID header, not an actual mac address. The UA is pointing this out in the form of a warning, although the specification only recommends a mac address, but does not require it. Not sure if this is in any way relevant to what I am experiencing.

As I stated in my introduction, if there is abetter forum to provide this feedback and ask these questions, please let me know.

Also, I just wanted to also say thank you to everyone working on this and let you guys know that I greatly appreciate the consideration and work that has gone into the Private Network Access Permission Prompt. I've been prototyping a web app for provisioning, configuring and controlling microcontroller based IOT devices on private networks and the Private Network Access Permission Prompt not only single-handedly rescues this project from being killed from the more restrictive insecure origin to private network access policies, but will now enable me to host my actual application on a secure origin significantly increasing security for my users and opening up access to more powerful web features like Service Worker, PWA, USB, Bluetooth and more. All things that will greatly improve the user experience and usefulness of my application. So, greatly appreciate it <3

@iVanlIsh
Copy link
Collaborator

iVanlIsh commented Nov 2, 2023

I put together a simple locally hosted nginx that simply responds to all requests with the Private-Network-Access CORS headers. In my very first attempt the response did not include the Private-Network-Access-ID or Private-Network-Access-Name headers on any requests, but the permission dialog still showed up and I was able to grant permission to connect to the local host. The fetch request that triggered the permission dialog completed successfully.

Since the specification -- as far as I understand -- says that a preflight request response without a valid targetId should result in a network error I was a little surprised that this worked at all. I believe that I did get a warning about the Private-Network-Access-ID being invalid in the console though.

Sorry for the inconvenience. I'm updating the explainer and spec to meet the current behavior. We decided to allow requests without id and name to make it easier for migrations but in a limited way. We only keep that permission for the current window context.

I then continued to add the Private-Network-Access-ID and Private-Network-Access-Name headers to my nginx config to see how the permission dialog changes when they are present. However, it is unclear to me how I can revoke the permission again. I went to the site permission settings and couldn't find anything there. I proceeded to reset all permissions and delete all site data, but I cannot get the permission dialog to re-appear. Not only is this obviously an issue for development and testing, but it also seems like a major oversight if the user cannot revoke their permission at a later point anymore. Please point me in the right direction if the permission is currently already revocable.

This looks weird. Did you try to close the current window and/or start a new one? The ephemeral permission should last for only the current window context.

Lastly, I was under the impression that the { targetAddressSpace: 'private' } options bag would only have to be provided on the first network request to the private origin server for the duration of the document lifetime.

This is not true. As a request without TLS and on the private network, the local origin/url to IP address mapping is so easy to change even within the same document lifetime. We intentional ask for specific request for relaxing mixed content check upon that.

Also, because of the collisions and churn with DHCP, the IP address for a private device can also easily changed. So we rely only on the ID provided by private device itself for long term permission storage.

I was under the impression that this mechanism would also allow for usage of other non-fetch request sources like HTML elements or XHR. At least for me currently that does not seem to be the case. I have to provide the { targetAddressSpace: 'private' } options bag on each fetch request for them to be succesfull.

Unfortunately this is not working. We aimed to support this in the future by following possibilities:

In addition I don't seem to be able to use other mechanisms to trigger successful network requests to the origin host. In my particular use-case I am intending on using Server Sent Events / EventSource and the requests being sent by EventSource fail due to the mixed content policy, even after having sent a fetch positively acknowledged fetch request with { targetAddressSpace: 'private' } options bag.

Dug a bit upon EventSource. EventSource's options is differed from fetch options and only option it currently support seems to be withCredentials.

Thanks for your reminding. I will consider if we also want to add this to EventSource options.

...It does not appear that having added these headers retroactively changed anything. Note, that for testing purposes I have set a random string as the Private-Network-Access-ID header, not an actual mac address. The UA is pointing this out in the form of a warning, although the specification only recommends a mac address, but does not require it....

Sorry for the inconvenience again. I will update the spec ASAP. The following restrictions are applied:

  • Private-Network-Access-ID should be a 48-bit value presented as 6 hexadecimal bytes.
  • Private-Network-Access-Name should be a valid name as a string that matches the [ECMAScript] regexp /^[a-z0-9_-.]+$/. 248 is the maximum number of UTF-8 code units in the name.

To the end: yes, this is the right place and thanks for your feedback! I'm really happy to find someone in the developers' community is trying it immediately after the rollout. Appreciate to every effect you put on this.

@iVanlIsh
Copy link
Collaborator

iVanlIsh commented Nov 2, 2023

Sorry wrong link for the walk through doc provided, please use: https://docs.google.com/document/d/1W70cFFaBGWd0EeOOMxJh9zkmxZ903vKUaGjyF-w7HcY/edit?usp=sharing

@DanielBaulig
Copy link
Author

Thank you for the details!

Just for clarification: the permissions without -ID and/or -Name only persist for the duration of the window context? I believe I did try restarting the session, but let me verify, just to be sure.

Also thanks for clarifying that the { targetAddressSpace: 'private' } options bag has to be provided on every request. That seems reasonable.

You mentioned using Service Worker to add the { targetAddressSpace: 'private' } option to fetch requests to support some other use-cases outside of direct usage of fetch (e.g. HTML elements or XHR). Should I expect this to work as it stands in Chrome Nightly or is this currently only a planned or considered feature? Sorry, it wasn't entirely clear to me from your response. If this should work right now, then I believe I should be able to get EventSource to work that way and will give that a shot.

Thanks for sharing the -ID and -Name requirements/formats.
I'm curious why it was decided to enforce the 48bit 6 hexadecimal byte (i.e. a mac address) format for the ID? While the mac address seems like a reasonable recommendation it seems very arbitrary to enforce it. It's not providing any actual guarantees that the value is indeed stable and unique, as previously required by the spec. As a user of the API I can still just drop a 00:00:00:00:00:00 or randomly generate 6 hex byte combinations. It doesn't do anything to help enforce the attributes that are really desirable. But it does limit the ability to use other reasonable options that are both stable and unique, e.g. a manufacturer name and serial number or a at provisioning time randomly generated and then stored identifier (e.g. a UUID/GUID). In fact, since a lot of IOT devices can be factory reset or even entirely re-flashed with different firmware using a randomly generated and then persisted GUID at time of provisioning seems to be the better unique identifier for this use-case. I wouldn't expect permission to connect to a local device to persist after that device was factory reset or completely reprovisioned to support a completely different use-case. Being more permissive with the -ID header would make it much easier for a manufacturer to implicitly revoke access to the device upon reset/reflash, while not having any apparent draw-backs compared to the currently enforced format. Using the network interface mac address would by default persist through such a reset / reprovisioning.

@DanielBaulig
Copy link
Author

DanielBaulig commented Nov 3, 2023

Quick update here from my side:

  • I tried using Service Worker to send a fetch with targetAddressSpace set to private, but it appears the option is currently ignored. Can I expect that this is something that will become available before the end of the Origin Trial or should I expect this to only become available after the Private Network Access Permission Prompt has shipped?
  • I tried restarting the browser but I seem to continue to have permission to access the origin that I originally whitelisted without a valid -ID. But at this point I am also not 100% sure if I maybe misremember some of the details. Since I am not sure how to explicitly revoke the permission again, I am not able to verify that what I believe happened is actually what happened. Some way of revoking the Private Network Access Permission again would be very helpful.
  • I tried testing the Service Worker API locally by enabling Enable Permission Prompt for Private Network Access in about:flags and returning the Content-Security-Policy: treat-as-public-address content security policy directive header. My understanding is that the CSP directive should instruct the UA to treat the document as if it was served from a public (vs local) address and as such restrict unpermissioned access to the private address space. However, the localhost domain still seemed to have unlimited access to the private network, even without { targetAddressSpace: 'private'} and I never saw a permission dialog show with that setup*. I had to push code to a public web server to actually test the Service Worker API.

(*) The behavior under these circumstances (site served from http://localhost, CSP directive treat-as-public-address applied) seems a little inconsistent. Here's everything I have observed using that setup:

  1. Sending fetch('http://privatehost') sends a preflight request with Access-Control-Request-Private-Network: true, which the privatehost server confirms with Access-Control-Allow-Private-Network: true as well as Private-Network-Access-{ID, Name} for an ID that has not been granted permission for the (localhost, privatehost) tuple yet. No permission prompt is shown and the following fetch request succeeds without issues.
  2. Sending fetch('http://privatehost', { targetAddressSpace: 'private'}) sends a preflight request with Access-Control-Request-Private-Network: true, which the privatehost server like before confirms with Access-Control-Allow-Private-Network: true as well as Private-Network-Access-{ID, Name} for an ID that had not been granted permission for the (localhost, privatehost) tuple yet. In this case the permission prompt is shown and if I cancel out of the dialog the subsequent fetch request fails.
  3. Sending fetch('http://privatehost') successfully sends the fetch request even if the preflight request fails with an 500 error and no valid Private-Network-Access or Access-Control-Allow-Private-Network response headers. No permission prompt is shown in this case either.

Edit 2:
I guess the behavior I am seeing when serving from localhost could be explained by the this quote from your explainer:

Note: Mixed-content with localhost is allowed, see webappsec-mixed-content spec.

My guess is because localhost is by default not subject to the mixed content restrictions the requests without { targetAddressSpace: 'private'} will not fail by default. I would assume that Private Network Access itself would prohibit any fetch requests with incorrect targetAddressSpace independent from mixed content policies. With treat-as-public-address active, localhost should not be able to send requests without targetAddressSpace if the address is actually in 'private' or 'local'.

@iVanlIsh
Copy link
Collaborator

iVanlIsh commented Nov 3, 2023

For the -ID stuff: Thanks for your opinion. It makes some sense for me. I will bring it to a internal discussion. We would like to restrict it strictly in first place as to limit the ability of manufactures choosing week or duplicate IDs for all their devices. As it is not possible for the browser to determine whether the private network environment changed or not, it might cause an unconscious allowance to a device with the same ID but in different place/internet environment for the users.

Service Worker: I would expect it to be available during the Origin Trial and in next one or two milestones.

Storage: It would be really weird that you can still access ephemeral. However, I do realise that it is inconvenient to not having the ability to revoke devices. This was on my list but with low priority. I will consider to move it forward.

Not able to use Content-Security-Policy: treat-as-public-address sounds like a bug. I will dig it more.

After all, thanks a lot for your detailed comments!

@DanielBaulig
Copy link
Author

Thanks a lot for your reply!

We would like to restrict it strictly in first place as to limit the ability of manufactures choosing week or duplicate IDs for all their devices.

I agree with the intention, I am just concerned that the restriction itself doesn't actually do that. A manufacturer can still easily choose a weak or even the same mac address formatted value for all of their devices. Just because it looks like a unique mac address doesn't mean it is one.

In fact, I would argue that if the mac address format requirement makes providing a unique and stable identifier harder for a manufacturer, having a strict requirement for the mac address format may make it less likely to get a stable and unique identifier. Instead of providing a unique and stable alternative to the mac address that may be more readily available in the particular code path, the manufacturer (or one of their developers that's just trying to get home for the weekend) may choose to just hardcode 12:34:56:78:9A:BC over figuring out how to get the interface mac address exposed to the OPTIONS request handling code. Generally and on average, people tend to choose the path of least resistance.

Again, I think using the mac address is a very good recommendation and a great default that should be possible under almost all realistic circumstances, but it's probably not always the best and or simplest option for device manufacturers to meet the actually desired attributes of unique and stable IDs. And that puts us quickly in a territory where making it harder to choose a reasonable alternative to the device mac address will lead to device manufacturers and developers choosing the easiest option instead (i.e. hardcoding some arbitrary mac address like string) and thus might have the opposite effect of what is actually desired.

I'm a strong believer in the idea of "what you incentivize will happen" and I'm afraid that an overly restrictive -ID format requirement will incentivize developers to do the opposite of what the spec authors and UA implementors would like to see (i.e. actually stable and unique IDs).

In the end the mac address format requirement isn't a show blocker for any alternative simply because it is technically impossible to enforce that the provided mac address formatted ID is actually indeed a valid mac address of the device. So if one wanted to provide some other unique and stable ID that doesn't "look" like a mac address one could conceive some mechanism to digest the alternative ID down (or blow it up) to 64 bits and just format it into a 6 hexadecimal byte string. I think the question ultimately isn't about what is or isn't possible given a more or less restrictive ID format, because on that front they are strictly speaking equivalent. Instead, the question ought to be what is more likely to happen in each scenario. And absent any actual data on that, it seems that is mostly a matter of opinion rather than fact or science.

And even though I've written a lot of words on this, I don't actually have a super strong opinion on it. I just believe that this is a case where being more restrictive might to lead to less desirable results.

Service Worker: I would expect it to be available during the Origin Trial and in next one or two milestones.

That's awesome and good to know. To continue testing my application I've cobbled together an EventSource polyfill that simply uses fetch underneath. Given that {targetAddressSpace: 'private'} support in Service Worker is already planned and expected to land in one or two versions of Chrome, I will continue testing with the EventSource polyfill for now but not actually bring it to production level quality.

Storage: It would be really weird that you can still access ephemeral. However, I do realise that it is inconvenient to not having the ability to revoke devices. This was on my list but with low priority. I will consider to move it forward.

I agree, it seems unlikely. Although I am wondering if returning a valid -ID during the same session at a later point may have caused an additional entry with valid ID to have been created that didn't show the permission dialog again (because the already existing ephemeral permission). This is speculation and was the scenario I wanted to test, but couldn't since I wasn't able to revoke any permissions already granted. I think a (presumably) simple stop-gap solution is to just wipe out all permissions when the site permissions are reset to default. A more granular and explicit interface is probably desirable before this leaves Origin Trial, but that would at least unblock some of this testing and debugging.

Not able to use Content-Security-Policy: treat-as-public-address sounds like a bug. I will dig it more.

Let me know if I can be of any help here. Happy to do some more testing on this.

@iVanlIsh
Copy link
Collaborator

iVanlIsh commented Nov 6, 2023

I tried Content-Security-Policy: treat-as-public-address myself, it should work.

Just fyi:
a. don't use it as a html meta, it will be ignored in that way.
b. Don't use localhost or 127.0.0.1 for the sub-resource, it will be recognised as secure context and will pass mixed content check.

@DanielBaulig
Copy link
Author

It is now working as expected for me, too. Not entirely sure what happened. Sorry for the confusion.

@DanielBaulig
Copy link
Author

I have another question/comment. This time around feature detection. The specification mentions that feature detection was considered, but later removed.

I'm facing the following challenge, which I believe could be resolved using feature detection and I wondered if there are any recommendations or thoughts on how to resolve it without.

I'm building a web application to provision, configure and control IOT devices. There are multiple ways to add a new IOT device into the application, but the technically least complex one is to type the IP address or hostname into an input form.

When a new unknown user loads up my application for the first time and chooses to manually add a new device there are fundamentally two possibilities.

  1. The UA supports Private Network Access and the permission prompt
  2. The UA does not support Private Network Access (and the permission prompt).

Ideally I would like to provide some form of feedback to my users if they cannot use the app due to lack of PNA (and potentially redirect them to an alternative, e.g. the right browser, a mobile app or similar), but I'm also concerned about not being able to distinguish between the cases of the UA not supporting Private Network Access and the permission prompt at all and the user simply mistyping the hostname / IP address. I think it would be a pretty bad user experience if I told my users "Couldn't connect to IOT device. Please make sure you provided the correct address or hostname." if in reality their UA simply does not support PNA. This would be a very frustrating experience for users. Similarly, the alternative might be to educate the user on the PNA feature and ask them to ensure their UA supports it. But a) this is a pretty tall order for an average user and b) would also be incredibly misleading and possibly frustrating if the problem really is that they have a typo in their hostname.

Now I fully understand that allowing the application to distinguish different private hosts/addresses and their permissions or being able to distinguish between fetch request failing for lack of permission or some other technical reason, can be abused for fingerprinting and other nefarious use-cases. I want to make sure to elaborate that this is not what I am suggesting / asking for.

But without the ability to tell if the UA supports PNA and the permission prompt at all, I don't see a good way to provide a reasonable user experience and guide my users in the right direction if the targetAddressSpace: 'private' request fails. Any suggestions to do so without feature detection would be greatly appreciated.

@DanielBaulig
Copy link
Author

DanielBaulig commented Nov 11, 2023

I just updated Chrome canary to "Version 121.0.6119.0 (Official Build) canary (arm64)" and was able to send requests to private target address space without seeing any OPTIONS requests. I was on canary, on a secure origin, had the origin trial active and also the about:flags version of the experiment active.

I took a video of it: https://www.youtube.com/watch?v=PBQVl4NznWY

  • First I send a request to a .local mDNS domain without { targetAddressSpace: 'private' }. This host I have previously granted permission using the permission prompt. As expected the request fails due to mixed content policy.
  • I then send a request to the same same destination but providing the correct targetAddressSpace: private option. The browser does not seem to send a preflight request. Nevertheless the request succeeds.
  • I then send a request to a random IP address, just to demonstrate that the issue seems unrelated to the fact that I had granted private network access permission to the origin before. The request remains pending because the host doesn't actually exist, but as you can see, again, there is no preflight request.

I tried enabling the other PNA related experiments in about:flags as well as deactivating all experiments and only using the origin trial as well as only activating the permission prompt experiment in about:flags and deactivating the origin trial. All of them producing the same result.

The only time I was able to achieve a different result was when I deactivated all experiments and the origin trial at which point the mixed content policy would restrict me from doing any requests to insecure origins, including those private network ones from above (as I would have expected).

@DanielBaulig
Copy link
Author

DanielBaulig commented Nov 13, 2023

I think I was able to narrow down what is causing the issue I've been seeing. I am experimenting with a SW on the origin that I've been testing on. My fetch event handler looks roughly like this:

self.addEventListener('fetch', (event) => {
  const request = event.request;
  if (!request.url.startsWith(location.origin)) {
    // Skip cross origin requests
    return false;
  }
  // ... handle other requests going to the same origin as the SW
});

These PNA requests are all going to origins other than the one the SW is served from and are using the early return code path (which I have verified). That said, when activating "Bypass for network" I do see preflight requests. When "Bypass for network" is not active and the early return SW fetch handler executes, I do not see preflight requests. This seems to be a bug, but then maybe my understanding here is wrong.

Update: I have some more details to share. It appears the difference is not caused by if the PNA request bypassing the service worker or not, but instead by if the document request bypasses the SW or not. If the document was loaded with "Bypass for network", then all the PNA requests will send a preflight request regardless of if they are bypassing the SW or not. But if the document is loaded with "Bypass for network" deactivated, then none of the PNA requests will trigger a preflight request. I'm going to look into what exactly is causing the difference. The document request should be served network first, so I don't think it's because the response was retrieved from cache, but I will verify this.

Update 2: I think I have a good reproduction, but only if not served from localhost. When served from localhost, even with Content-Security-Policy: treat-as-public-address active (or maybe because?) it will correctly send preflight requests.

The following seems to reproduce the issue:

self.addEventListener('fetch', (event) => {
  const request = event.request;
  if (!request.url.startsWith(location.origin)) {
    // Skip cross origin requests
    return false;
  }
  event.respondWith(fetch(request));
});

I.e. skip handling any cross origin requests in the SW (including PNA requests) and explicitly send other requests (including the document request) onto the wire using fetch. With this setup the I will not see any preflight requests sent for { targetAddressSpace: 'private' } requests. If you bypass the SW for the document request or if you skip handling the document request in the SW it will work as expected.

Here's a minimal setup to reproduce: https://www.danielbaulig.de/pna-sw-bug/
Load the page, confirm the prompt with any private host/IP address. You will see that no preflight request is sent.
Make sure to enable Permission Prompt for Private Network Access experiment in about:flags, since this origin is not part of the origin trial.

@DanielBaulig
Copy link
Author

DanielBaulig commented Jan 28, 2024

I just want to call out that as of updating to Chrome 121 I can exploit this in production and am able to send mixed content network requests to private network hosts from my PrivateNetworkAccessPermissionPrompt Origin Trial website at https://neewer.rarelyunplugged.com/ without a permission dialog showing up at all. My guess is that this would be true for anyone participating in the PrivateNetworkAccessPermissionPrompt Origin Trial.

aarongable pushed a commit to chromium/chromium that referenced this issue Feb 1, 2024
As discussed[1], restricting private-network-access-id to MAC
address-ish style won't improve security. It should be designed and
maintained by device providers.

[1] WICG/private-network-access#117

Bug: 1494426
Change-Id: Ib97a70269d701ce7489cccc0089754abebe5d42c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5256386
Reviewed-by: Jonathan Hao <phao@chromium.org>
Commit-Queue: Yifan Luo <lyf@chromium.org>
Reviewed-by: Yifan Luo <lyf@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1255021}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants