Proposal: Add a dedicated Content Security Policy (CSP) API for web extensions
nir-walkme opened this issue · comments
Summary of the proposal:
Provide a dedicated API for an extension to read and write the Content Security Policy (CSP) of a page. It should work consistently regardless of how the CSP is configured (HTTP Header or <meta>
element).
Motivation:
- Previous discussions: There seems to be a consensus among browser vendors that such an API is required. It was discussed during the WECG Meet-Up which I did not attend, so I am adding this comment as a reference.
- Security: I think this is an important point which gets overlooked. Since blocking webrequest is no longer available in Chrome MV3, developers are forced to use DNR as an alternative. Since it is not possible to edit (read, change, write) the CSP header using DNR, some developers might resort to the easy solution and just remove the CSP header completely using DNR. This is the easiest option to the developer but it is very risky as the website is now not protected by CSP.
- Consistency: Content Security Policy is a very important mechanism and it should be treated as such by the extensions. The current way of handling it using webrequest feels like a workaround and I think it should have its own dedicated API. Moreover, the fact that CSP can also be defined using a
<meta>
element (and maybe there will be other methods in the future) just shows that it needs its own dedicated API.
Proposed API:
browser.csp.get(frameId)
Returns a dictionary of the directives of the page’s current Content Security Policy. The page is identified by the frameId.
browser.csp.set(frameId, dictionary)
Sets the Content Security Policy for the page. The page is identified by the frameId.
Validation of dictionary:
If the dictionary contains a key which is not supported by the browser (for example: {'example-src' : 'example.com'}
), browser.csp.set should still work but the browser will ignore that key. This key might be supported by other browsers so the action should not fail.
If the dictionary contains an invalid value (for example: {'img-src' : 'example.com ;;; example.org'}
), browser.csp.set should still work but the browser will ignore that key. This key might be supported by other browsers so the action should not fail.
browser.csp.onCSPChange.addListener(eventHandler)
The event handler would be called whenever the CSP of the page changes.
Example cases: Page load, page adding a <meta>
CSP element or another extensions changes the CSP.
Usage example:
var currentCSP = browser.csp.get(123);
/*
currentCSP ==
{
'default-src' : "'self'",
'img-src' : "*",
'media-src': "example.org example.net",
'script-src': "userscripts.example.com"
}
*/
currentCSP['media-src'] += " example.com";
browser.csp.set(123, currentCSP);
@rdcronin @carlosjeurissen
I submitted this proposal as a follow up from the discussion in #440
Happy to hear your thoughts on this
Thank you for discussing my issue.
I missed the issue getting into the agenda so I didn't attend the meeting.
Wanted to follow up with some points after reading the meeting notes:
My main use case is to relax CSP and not to make it more strict.
It was mentioned that it might not be possible or desirable to allow CSP relaxation.
Regarding technical feasibility:
It is already possible in all browsers to relax CSP using DNR, by setting or removing the content-security-policy header.
Per the browsers' implementation, it might be possible to relax CSP only on page load when the CSP gets initialized and not afterwards. If this is the case, I am OK with the proposed API working only on page load. I would be happy to get input from the browsers on this.
In regards to the question if the platform should allow relaxing CSP:
This is a good question in theory and I can see why browsers would not want to do that.
But, in practice, it is already possible to remove the CSP completely using DNR so the ship has already sailed.
I think that without giving a proper method to modify the CSP, some developers might resort to the easy solution and just remove the CSP header completely using DNR. So I think that implementing my proposed solution would improve the overall security state of the platform.
Thank you for the proposal, @nir-walkme !
Overall, I'm supportive of providing an API for targeted CSP modification. I think there's a lot of pieces we'd need to discuss here in terms of what the API surface would look like and the implementation it may provide. A few immediate notes:
- We'd want this to be async (because I think this would be restricted to privileged extension contexts). That's an easy change. : )
- I think the type of the CSP object is something we'd want to discuss. Here, it's presented as an object with single keys for each directive; however, CSP isn't quite that simple. It allows for multiple CSPs per-frame -- either via multiple headers, multiple tags, or a combination of both. Because of this, I think we'd definitely want to have an array of CSP values, rather than a single one. Related to that, do we allow modifying a given CSP (e.g., a specific meta tag)? How do we identify those, if so?
- What is the behavior when modifying the CSP? e.g., if modifying CSP provided by a tag, does the tag's content change, or is it kept the same?
I think we'll need to iron out a lot of these details more before we move onto the API specification. I'm supportive of this in general, but unfortunately, won't have bandwidth to drive it directly. It looks like Safari is neutral on the API. @Rob--W , would you want to work with @nir-walkme to flesh out this proposal more and sponsor it?
@nir-walkme Thanks for initiating this!
There are some more open ends. What if a frame is already loaded? What would happen to the page? Also one would need to find out the frameId which is as far as I know only supported in Firefox right now. Going for a matches pattern can be a good alternative. Which is closer to existing implementations (dnr/webrequest). Actually this could also be part of the contentScript API.
@rdcronin As for helping directing to implementable proposals. To cover more usecases. Could we think of an API design similar to the blocking webRequest.onHeadersReceived
API to deal with this? Which would include the headers specified in http-equiv? This would also cover other usecases covered by #440 like changes to the permissions-policy
header.
As for dealing with multiple CSPs. I don't think we actually need this for the extension developer. We can create some high-level API which abstracts away from this. Speaking from my side as developer, I either want to add or remove values to all CSPs. In the case of adding, the browser can enforce an additional CSP. In the case of removing values from directives, it can remove values from each CSP. If a directive relies on a fallback, a copy of the fallback would be made without said values. If a complete directive should be removed, all fallback directives will be removed as well. Other directives which fallback on the same directive would be cloned from the fallback. This would also help with multiple extensions messing with the CSPs and for cases in which a website decides to update their CSP. I do not think such high level API would miss any usecases.
I suggest adding an issue to CSP working group https://github.com/w3c/webappsec-csp/issues to collect their thoughts.
I would like to add my 2 cents to this proposal. Working for an extension used for a DAP (digital adoption platform), I should have the same constraints as @nir-walkme.
A DAP is a layer that is added on another web application to add walkthroughs, contextual help and so on. DAP clients usually deploy it on one or more of the web applications they internally use. Most of those web applications do not plan for the fact that others may want to inject code in it, so we have to resort to an extension to inject the DAP layer.
- In Manifest v2 we could easily use a blocking webRequest.onHeaderReceived to add the DAP server URL to the required CSP directives (script-src, style-src, connect-src...) to keep the app security while still allowing the DAP layer to run.
- In Manifest v3, in Chrome and Edge, only force installed extensions can use the webRequest API, while not all our clients deploy it that way, and we would like to avoid the workaround of totally removing the CSP headers.
Is anyone working on it at the moment ?
When designing the API, we should not forget about Content-Security-Policy-Report-Only : developers may want to fix CSP and/or CSP-Report-Only independently.
@aconstancin not sure anyone is actively working on an API design let alone an implementation. I shared some thoughts here #574 (comment). You are free to contribute.
As for reporting. My suggestion would be to completely disable the report-uri when extensions modify the CSPs as reports would not necessarily be the cause of the authors set CSP. Thus the Content-Security-Policy-Report-Only
header would just be ignored if an extension starts changing any CSP.
@nir-walkme Thanks for initiating this!
There are some more open ends. What if a frame is already loaded? What would happen to the page? Also one would need to find out the frameId which is as far as I know only supported in Firefox right now. Going for a matches pattern can be a good alternative. Which is closer to existing implementations (dnr/webrequest). Actually this could also be part of the contentScript API.
+1 for patterns
@rdcronin As for helping directing to implementable proposals. To cover more usecases. Could we think of an API design similar to the blocking
webRequest.onHeadersReceived
API to deal with this? Which would include the headers specified in http-equiv? This would also cover other usecases covered by #440 like changes to thepermissions-policy
header.As for dealing with multiple CSPs. I don't think we actually need this for the extension developer. We can create some high-level API which abstracts away from this. Speaking from my side as developer, I either want to add or remove values to all CSPs. In the case of adding, the browser can enforce an additional CSP. In the case of removing values from directives, it can remove values from each CSP. If a directive relies on a fallback, a copy of the fallback would be made without said values. If a complete directive should be removed, all fallback directives will be removed as well. Other directives which fallback on the same directive would be cloned from the fallback. This would also help with multiple extensions messing with the CSPs and for cases in which a website decides to update their CSP. I do not think such high level API would miss any usecases.
Yes, it would be ideal to not have to care wether the CSP comes from header, meta or a mix of both!
The way we do it presently in Manifest v2 is to fix all the CSP headers we find so that even if multiple cascading script-src are found we are sure they all contain the fix we want. Not elegant but it does work well 😄
Having a new specific DNR action we could maybe do something like:
{
"id": 1,
"priority": 1,
"action": {
"type": "content-security-policy",
"content-security-policy": {
"script-src": { "add": ["'unsafe-inline'", "https://my.site"] }, // <---- would copy default-src if no script-src if defined in the page various CSPs but default-src is
"style-src-elem": { "add": ["'unsafe-inline'", "https://my.site"] },
"connect-src": { "add": [....], "remove": ["'none'"] },
"font-src": { "set": "*" }
},
"condition": { "urlFilter": "foo.bar", "resourceTypes": ["main_frame"] }
}
Ideally, priority 1 CSP rules should be executed at the end to make sure a lower priority rule does not override it...