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

Proposal: API to embed pages in WebExtension bypassing CSP #483

Open
OlegWock opened this issue Nov 4, 2023 · 23 comments
Open

Proposal: API to embed pages in WebExtension bypassing CSP #483

OlegWock opened this issue Nov 4, 2023 · 23 comments
Assignees
Labels
neutral: safari Not opposed or supportive from Safari proposal Proposal for a change or new feature supportive: chrome Supportive from Chrome topic: csp Related to content security policy enforcement

Comments

@OlegWock
Copy link

OlegWock commented Nov 4, 2023

Problem

Some extensions would like to embed third-party sites inside the extension interface (popup / side panel / separate extension page). Currently, this is done using <iframe>, but since most sites restrict embedding them in iframes (using Content-Security-Policy or X-Frame-Options), extensions need to resort to different workarounds to make it work.

Current solution

The common current solution is to use declarativeNetRequest (Manifest V3) or webRequestBlocking (Manifest V2) to intercept requests to the target site and modify (or strip) CSP and X-Frame-Options headers to allow embedding pages in iframes.

This approach, however, has a number of problems:

  • If the developer is not careful enough, this overwrite might weaken or disable the CSP of the target site not only for the extension but for other sites too, risking the end-user’s security.
  • declarativeNetRequest and webRequest don't intercept responses served by the service worker; those responses will contain the original CSP header. The only way for the extension to work around this is to remove any registered service workers for the website (using browsingData API) each time before embedding it into an iframe.
  • Sites inside an iframe won’t receive cookies with SameSite=Lax (default value) or SameSite=Strict (they should be explicitly marked as SameSite=None to work). This often leads to the site treating the user as logged out, blocking access to most of the site's features.

Ideal solution

The ideal solution should:

  1. Allow extensions to embed third-party sites without explicit permission from the site (no opt-in) and without a way for the site to prohibit embedding it in the extension (no opt-out).
  2. Embedded pages should be top-level, and from the perspective of an embedded page, it should look like the user just opened the site in a browser tab (i.e., no window.top or other links to parent context).
  3. An embedded page should share a storage partition with pages from same origin, as if it was opened in a separate tab. I.e. cookies, localStorage, OPFS and other data specific to origin should be shared.
  4. Existing limitations for extensions should apply:
    • Any registered content scripts matching the URL in the frame should be injected there.
    • If extension doesn’t have host permission for the page in frame, it shouldn’t be able to interact with the page or exfiltrate any data from it (e.g. redirect URL).

Possible solutions

There are a few novel embedding techniques (in addition to existing iframe). Unfortunately, neither of them fully satisfies criterias from previous section.

Fenced Frame

Fenced frames allow embedding cross-origin pages and enforce boundaries between embedder and embedded contexts. However, to be displayed in a fenced frame, the site needs to opt in by providing Supports-Loading-Mode: fenced-frame header in the response. And because of this, a fenced frame doesn’t solve the original problem, as it lets site owners control if their site can be displayed in a fenced frame, and I expect most of the sites will prohibit it.

Controlled Frame

Controlled frame is a feature of Isolated Web Apps (IWA) that allows developers to embed a page into their app, bypassing the CSP or X-Frame-Options of the embedded site. However, there are a few important features of controlled frames that aren’t acceptable in a web extension context.

IWA can access and monitor user actions in a controlled frame: extract cookies from the frame, observe activity like keyboard events, etc. This is acceptable for IWA since a site open in a controlled frame gets its own storage partition, so a page in a controlled frame (and so IWA) won’t get access to cookies and other data associated with origin as if it was open in a separate browser tab.

This doesn’t work well for web extensions, as extensions are more integrated into the browser UI than IWAs and users expect to see the same version of the site in the extension and in a separate tab. Having separate storage partitions makes it impossible.

And even if controlled frame in extensions will still use a separate partition, allowing extensions to control frames for hosts they don’t have permissions for will also go against the current security model for web extensions.

Portals

Portals are intended for the pre-rendering of content and are even more restrictive than iframes: users can’t interact with content, the embedded page can’t access any API that requires permissions, etc.

Webview

Webview (docs 1, 2) is a special tag that is available for Chrome Apps (which are now deprecated) and allows embedding of third-party sites inside the app. Embedder can control embedded page to great extend: inject CSS/JS, listen to events like console messages or manipulate history. Embedder is also able to control which partition webview should use to store user data.

Taking into consideration that it's already implemented in Chrome and closely matches our requirements, webview looks like most realistic of current solutions. Though it's important to note, that Chrome Apps required separate webview permission to use this element. For extensions this behavior should be altered: using webview shouldn't require separate permission, but to have access to site inside webview, extension should have respective host permission.


So far webview seems to be the closest to ideal, however it still requires modifications to work well in extension's context. With this info I'd like to propose 4 different solutions for discussion.

  1. Making <webview> available for extensions and altering its behavior to better match extension's security model.
  2. Altering behavior of controlled frame if rendered in extension's context: use same storage partiotion and limit extension's access to embedded page
  3. Adding new extension-only attribute to iframe which if present will alter iframe behavior so it behaves more like browser tab (bypassing frame-ancestor CSP, all cookies are sent to page, no window.top, etc)
  4. Adding new HTML element <isolatedframe> which will behave as described in 'Ideal solution' section and will be available only for extensions.
@xeenon xeenon added topic: csp Related to content security policy enforcement proposal Proposal for a change or new feature and removed needs-triage labels Nov 4, 2023
@dotproto
Copy link
Member

dotproto commented Nov 5, 2023

Thanks for opening this. We've discussed the possibility of giving developers more control over CSP in declarativeNetRequest, but as I recall Chrome objected to this because CSP is not only controlled by request headers; it can also be set in META tags inside a document's response body. In addition, there were other challenges associated with embedding another page in an iframe that you've already called out. At the time, a suggestion was made to open a new issue to track the more generic request to improve embedding other web pages inside an extension page, but I don't think we actually field such an issue.

  • declarativeNetRequest doesn’t intercept responses served by the service worker; those responses will contain the original CSP header. The only way for the extension to work around this is to remove any registered service workers for the website (using browsingData API) each time before embedding it into an iframe.

This is also true of webRequest. Both APIs were designed as an abstraction over the browser's network layer rather than as a generic interception mechanism for requested initiated by a page or worker context.


I'd like to add another possible solution to the list

WebViews

The Chrome App platform introduced the concept of a <webview> tag "to actively load live content from the web over the network and embed it in your Chrome App" (docs). This tag is use in chrome:// pages and in Chrome Apps (deprecated). This element is also exposed in Electron (docs). See this Igalia blog post for additional notes about the tag.

There was some discussion about bringing the webview tag to Extensions in issue 422805. This issue was closed as WontFix in 2015.

@hanguokai
Copy link
Member

Just add another discussion link. We discussed this problem a few months ago, including webview, Controlled Frame and Fenced Frames.

@oliverdunk
Copy link
Member

Thanks so much for filing this @OlegWock. It is by far the most comprehensive summary of the situation that I've seen! I suspect we are going to need quite a bit of discussion here but this is definitely a good starting point.

@OlegWock
Copy link
Author

OlegWock commented Nov 6, 2023

@dotproto thanks, I updated original comment to include info about webview (& fixed some typos)

I didn't know about <webview> but it looks really promising

@oliverdunk oliverdunk added the follow-up: chrome Needs a response from a Chrome representative label Nov 9, 2023
@xeenon xeenon added the neutral: safari Not opposed or supportive from Safari label Nov 9, 2023
@dotproto
Copy link
Member

Thinking out loud, what if we tweaked how browsers handle CSP and X-Frame-Options? If an extension has host permissions for a site and a specific (new?) permission, we could treat top-level browsing contexts on the extensions origin as an allowed ancestor for that site. For CSP, perhaps the extension's origin could be implicitly included in the ancestor-source-list for each frame-ancestors directive in the page's CSP list. For X-Frame-Options, perhaps we could simply bypass the frame ancestor check.

@OlegWock
Copy link
Author

How would this work with frame busters? Currently, even if CSP is patched, page can figure out if it's displayed in iframe and refuse to load, for example.

And if I understand correctly, with this approach, browser won't pass SameSite=Lax/Strict cookies to the site?

@oliverdunk
Copy link
Member

How would this work with frame busters? Currently, even if CSP is patched, page can figure out if it's displayed in iframe and refuse to load, for example.

An open question we have is what the scope of this change should be. Bypassing CSP and X-Frame-Options works in most cases where a site is just trying to avoid other sites embedding it, and would be significantly easier than creating a true frame-buster proof mechanism. While I can see the value in both it may be worth pursuing the former first since something is better than nothing.

And if I understand correctly, with this approach, browser won't pass SameSite=Lax/Strict cookies to the site?

I think this could be solved separately. There are similar concerns with third-party cookie deprecation that we may be able to solve with host permissions (see where this link goes and the storage section in the same doc): https://developer.chrome.com/docs/extensions/mv3/storage-and-cookies/#cookies-partitioning

@oliverdunk
Copy link
Member

We discussed this at our in-person meeting in San Diego. There are two related goals:

  1. The ability to embed a website which does not wanted to embedded in general, but is not actively trying to block extensions.
  2. The ability to embed a website such that it cannot tell it is being embedded.

We agreed that it is desirable to solve both but that (1) is much easier than (2), and would solve a significant number of cases we have heard from developers in a much shorter time frame, so we'd like to start with that.

Our preferred approach for this is to simply ignore restrictions like CSP, X-Frame-Options and COEP. This would require host permissions, and initially we will only implement this if the iframe is in a top-level extension frame to avoid possible attacks that involve an extension page being embedded in a third-party site (even with an extension page and a.com > extension page -> b.com, a.com can navigate b.com unexpectedly). We think it makes sense to have this behavior without needing to opt-in for simplicity.

I'm going to take the action item of writing a more formal proposal for this.

@oliverdunk oliverdunk self-assigned this Mar 20, 2024
@oliverdunk oliverdunk added supportive: chrome Supportive from Chrome and removed follow-up: chrome Needs a response from a Chrome representative labels Mar 20, 2024
@yankovichv
Copy link

It seems like today is Christmas :)

The first option is very good. It will solve most of the difficulties.

However, will there be problems with cookies because the site frame is in the extension frame?

However, the clever Spotify developers make me ask another question: are there any plans for a second option? Will it develop, or most likely not?

@oliverdunk
Copy link
Member

@yankovichv, glad this sounds good 🎉

However, will there be problems with cookies because the site frame is in the extension frame?

At least in Chrome, frames rendered immediately below a top-level chrome-extension:// page always get cookies as though they were the top frame. There's some more on this here.

However, the clever Spotify developers make me ask another question: are there any plans for a second option? Will it develop, or most likely not?

Everyone seemed supportive. We also all agreed it was a lot trickier though, so I'm not sure if anything will happen in the short term.

@hanguokai
Copy link
Member

I stumbled across a Chromium source code comment and a bug.

// Extensions can load their own internal content into the document. They
// shouldn't be blocked by the document's CSP.
//
// There is an exception: CSP:frame-ancestors. This one is not about allowing a
// document to embed other resources. This is about being embedded. As such
// this shouldn't be bypassed. A document should be able to deny being embedded
// inside an extension.
// See https://crbug.com/1115590
bool ShouldBypassContentSecurityPolicy() {}

So you are suggesting to apply CSP for frame-ancestor no matter the extension?

Yes.

Mike suggested to do it only if the extension have access to the embedded document. I am not sure to see what it really means. Is there any meaningful context were we can say that?

Yes. There is a concept of host permissions which an extension can specify and a user can modify (https://developer.chrome.com/extensions/runtime_host_permissions). IIUC the suggestion was for the extension to be able to embed a frame if it had access to a frame regardless of X-Frame-Options/frame-ancestors. I think this is a good thing to do given that:

  • There are legit use cases for an extension to be able to embed a frame and if it has host permissions to a frame then its reasonable to bypass the frame-src and X-Frame-Options restriction.
  • Currently if an extension has access to a page, it can already modify its X-Frame-Options and CSP header using the web request API to make this possible. However this is not ideal and in Manifest V3, we are hoping to prevent the extension from relaxing the CSP. If we automatically allow the extension to embed frames to which it has permission, it won't need to modify these headers.

In my understanding, this is consistent with what Oliver said earlier. At present (after that bug was fixed), extensions do not allow bypassing web page's X-Frame-Options/frame-ancestors. But ideally, if the extension has the page's host permission, embedding iframe should be automatically allowed. This can prevent developers from forcefully relaxing CSP.

@dscham
Copy link

dscham commented Apr 28, 2024

Hi, I stumbled on this discussion because CSP and X-Frame-Options just stopped my idea in it's tracks.

I get why those are useful for normal web use. I even get why they should work for extensions. On the other hand, since you could bypass them anyway, by changing the response in a worker, I don't see why to obey them at all in an extension.
Of course, you could see it the other way around and block being able to change those at all. But that also limits some use cases.

Mine is that I have hundreds of tabs. Some, which I probably won't need anymore. So I wanted to gamify cleaning them up. And for that, I wanted to preview the tabs contents in my extension. By opening a tabs URL in an iframe, one tab at a time.

And I know, I'm not the only person that uses their tabs as reminders or notes, of what to look into. But at some point realize, they have a lot of tabs they don't really have an overview of anymore and need to clean them up in a fun way.

I guess I'll have to bury that idea for now.

@safinaskar
Copy link

@dscham , try this code: https://stackoverflow.com/questions/74391398/mv3-declarativenetrequest-and-x-frame-options-deny . It somewhat works. (But you still need to somehow deal with issues listed in the first comment: #483 (comment) )

@safinaskar
Copy link

@dscham
Copy link

dscham commented Jul 15, 2024

Too bad it will have to be a kinda hacky solution. But thanks @safinaskar

@safinaskar
Copy link

@dscham , @OlegWock and everybody else.

I just wrote Chrome manifest v3 extension, which successfully embeds some sites in iframe. Not only the extension removes headers X-Frame-Options and Content-Security-Policy, but also applies many other hacks to get real-world sites working.

(Yes, I'm aware that just removing whole header Content-Security-Policy is bad idea. This is just demo.)

Here is my blog post and source code: https://safinaskar.writeas.com/mv3-chrome-extension-with-iframe-which-embeds-any-site .

This extension clearly illustrates how many hacks are required. If a site sets X-Frame-Options: deny, then it expects it will be always loaded to top frame and relies on this. For example, https://vk.com uses window.parent === window check to determine that given vk.com frame is top-level frame of vk.com . (So I had to patch window.parent).

So, I proved that just allowing to load everything in iframe will not work. We need some element, which behaves as top-level context.

@oliverdunk

Our preferred approach for this is to simply ignore restrictions like CSP, X-Frame-Options and COEP

As I just said, this will not work. You also need to do large changes in <iframe> behavior, i. e. to make it behave as top-level frame. I think it is easier to add new element or to patch some more suitable element, for example, <fencedframe> or <portal> (assuming it will be supported everywhere, of course).

@dscham , as well as I understand your use-case doesn't need interactive frames. You just need preview. As well as I understand this is exactly what <portal> are designed for! Another idea: if that tab is already opened, you can capture image/video using chrome.tabs.captureVisibleTab or chrome.tabCapture and then show it to user.

@OlegWock , as well as I understand, your use case is similar, so I hope this advice applies to you, too.

@OlegWock

Sites inside an iframe won’t receive cookies with SameSite=Lax (default value) or SameSite=Strict (they should be explicitly marked as SameSite=None to work)

As well as I understand, this problem doesn't appear here, see https://developer.chrome.com/docs/extensions/develop/concepts/storage-and-cookies#cookies-partitioning

@oliverdunk
Copy link
Member

As I just said, this will not work. You also need to do large changes in <iframe> behavior, i. e. to make it behave as top-level frame.

This is something we're aware of and discussed as part of the work here. I discussed it a bit in my previous comment. There are certainly cases like the ones you shared where window.parent === window is used. On the other hand, there are also sites which have just the X-Frame-Options header.

Finding a solution to embed content in a way that mimics a top-level frame is something we are all supportive of in principle, but don't expect we will be able to move forward short term. With that in mind, there are two options in the short term:

  1. In certain cases (on a top-level extension page), ignore certain headers. This isn't a full solution but helps in some cases.
  2. Do nothing until we can implement a full solution.

(1) certainly seems desirable, and doesn't prevent us from doing more in the future.

@dscham
Copy link

dscham commented Sep 14, 2024

@dscham , as well as I understand your use-case doesn't need interactive frames. You just need preview. As well as I understand this is exactly what <portal> are designed for! Another idea: if that tab is already opened, you can capture image/video using chrome.tabs.captureVisibleTab or chrome.tabCapture and then show it to user.

The way I understood it, doesn't that require that I bring the tabs to foreground and wait for them to load? That would be quite a jarring experience, which is exactly what I want to get rid of. Having the browser jump around between tabs, possibly hundreds of times, doesn't sound like an improvement over having to click through them.

@safinaskar
Copy link

safinaskar commented Sep 14, 2024

@dscham, you have these options:

  • Just use <portal>. This is something like iframe, but it is top-level context and it has no interactivity. (But first check how widely it is supported)
  • Create offscreen document ( https://developer.chrome.com/docs/extensions/reference/api/offscreen ) and then capture it. But I'm not sure whether this will work
  • If you already has tab opened, then capture it using chrome.tabs.captureVisibleTab or chrome.tabCapture. I don't know whether this will require activating those tabs

First two ways never bring anything to foreground and don't require the tab to be opened.

I don't have any additional information. So, you have to test these methods. I don't know whether they will really work

@safinaskar
Copy link

Long term solution to original problem (i. e. embedding top-level contexts in pages) seems to be controlled-frame ( https://github.com/WICG/controlled-frame )

@Hypnosphi
Copy link

If the developer is not careful enough, this overwrite might weaken or disable the CSP of the target site not only for the extension but for other sites too, risking the end-user’s security.

What's the right way to do it only for requests originating from the extension itself? I'm trying to use conditions.initiatorDomains for it but can't figure out what should it be for the offscreen page

@oliverdunk
Copy link
Member

I'm trying to use conditions.initiatorDomains for it but can't figure out what should it be for the offscreen page

Hmm, that sounds like the right approach. Are you just putting the extension ID here?

If you're still having trouble the mailing list would be a good place to pick this up to avoid adding too much noise on GitHub :)

@Hypnosphi
Copy link

Are you just putting the extension ID here?

Yes, [chrome.extension.id]. I've opened a conversation in the group, thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
neutral: safari Not opposed or supportive from Safari proposal Proposal for a change or new feature supportive: chrome Supportive from Chrome topic: csp Related to content security policy enforcement
Projects
None yet
Development

No branches or pull requests

9 participants