Injecting external scripts into page context

2,062 views
Skip to first unread message

Markus Seiffert

unread,
Aug 26, 2022, 5:20:45 AM8/26/22
to Chromium Extensions
Hey everyone,

we're currently working on migrating our Chrome-Extension from manifest v2 to v3. The extension adds some functionality (buttons, sidebars etc.) to linkedin.com, google.com and other websites.

Right now, in v2, we create a script tag within a content script which loads an external script from our server. (written by us, 100% under our control) and inject it into the page. The script is loaded, executed in the page context, buttons appear and communicate on click with the extension via CustomEvents. Thats perfect.

With manifest v3 however, we get an error on some pages because of a content security policy violation. Our server domain is obviously missing in the CSP of the current page.
I saw some examples that edit the content-security-policy header of the current page and add their own server to it but is this the best practice in this case or is there a better workaround?

Not sure if this would even help but we don't want to bundle these scripts loaded from our server into the chrome-extension because updates of the external pages (LinkedIn, Google etc) which could break our buttons would need a new chrome-extension release. This would add unnecessary delays to our fixes. (Some extension stores take up to one week from bundle upload to final release)



T S

unread,
Aug 26, 2022, 6:08:33 AM8/26/22
to Chromium Extensions, net...@codedcomplexity.de

Even if you could talk to your server in a content script, can't you simply not execute arbitrary code full stop?  How are you running an unsandboxed downloaded script in v2?

Markus Seiffert

unread,
Aug 26, 2022, 7:13:24 AM8/26/22
to Chromium Extensions, T S, Markus Seiffert
In v2 we have a content-script that is injected into the page and this content-script creates a script tag (document.createElement('script')) that is injected into the page's head.

The content-script does smth. like this:
  const scriptElement = document.createElement('script');
  scriptElement.src = 'https://myserver.com/assets/script.js';
  document.head.appendChild(scriptElement);

This works perfectly fine in v2 but not in v3. (Never thought about why but script tags created / injected from the content-script context in v2 are probably not restricted by the content-security-policy)
The injected script does not have the chrome.* api and needs to communicate with the extension/content script via CustomEvents and document.dispatchEvent.

The main reason for this arbitrary code that is loaded is, as i said above, the chrome-extension release/review delay. 
If the host page changes their HTML which breaks our functionality, we don't want to wait a week until a fix is released. (By that time a different thing could have changed and we would have to wait with the fix until the last release is live).

We do not want / need the chrome api in these external scripts. Communication via defined CustomEvents is perfectly fine.

Markus Seiffert

unread,
Sep 8, 2022, 5:09:37 AM9/8/22
to Chromium Extensions, Markus Seiffert
Bump. Does anyone have any tips on how fix the content-security-policy header issue?

wOxxOm

unread,
Sep 8, 2022, 6:13:56 AM9/8/22
to Chromium Extensions, net...@codedcomplexity.de
You'll have to run this code in page context not in content script context. It won't bypass the CSP of the page though so you'll also need to augment or strip the CSP header of the site. The simplest method is to use chrome.scripting.registerContentScripts([{id: 'foo', world: 'MAIN', js: ['inject.js'], matches: ['........']}]) where inject.js creates the script element for the remote script. Note that this may be viewed as a violation of the webstore's remote code policy.

Markus Seiffert

unread,
Sep 8, 2022, 6:31:26 AM9/8/22
to Chromium Extensions, wOxxOm, Markus Seiffert
Thank you for your response.

That's basically what i'm already doing. I have a contentscript (declared in my manifest.json) that is injected into relevant pages and creates a script tag which loads the code from our server. This code then runs in the Page context and communicates with the contentscript via custom events (document.dispatchevent).

So my only bet is to edit the CSP and add our server to the 'script-src' entry. I was hoping that there is another way of injecting external code into pages which is not limited by CSP. But if there isn't i have to deal with this header then. 

wOxxOm

unread,
Sep 8, 2022, 6:36:54 AM9/8/22
to Chromium Extensions, net...@codedcomplexity.de, wOxxOm
No, you're missing the crucial step: this code should ALREADY be in page context, whereas yours is still in content script context./

Markus Seiffert

unread,
Sep 8, 2022, 6:57:45 AM9/8/22
to Chromium Extensions, wOxxOm, Markus Seiffert
I just tried it and got the same result.

I did the following steps:
- removed the content script from the manifest
- added this `chrome.scripting.registerContentScripts([ { id: 'demo-script', js: ['scripts/contentScripts/inject.js'], matches: ['https://www.linkedin.com/*'], world: 'MAIN' }]) ` to my background.js

The inject.js file simply creates a new script (`document.createElement('script')`) and set's the URL to a script on our server.

But when i open LinkedIn now i see the same CSP error:
> Refused to load the script 'https://static.my-server.com/external-script.js' because it violates the following Content Security Policy directive: "..."

Adding a dynamic declarativeNetRequest rule works and fixes this issue but i was wondering if there is another way. 

wOxxOm

unread,
Sep 8, 2022, 7:21:53 AM9/8/22
to Chromium Extensions, net...@codedcomplexity.de, wOxxOm
Since nothing's changed for you it means your code wasn't running in a content script context, otherwise it wouldn't succeed on any page at all because in ManifestV3 a script element created in the content script context cannot point to a remote server. Barring a bug in Chrome, of course.

The CSP header still needs to be stripped or augmented to allow execution because your script element doesn't belong to the extension context. It previously did in ManifestV2, which automatically excluded it from the CSP of the page. BTW, the fact that we can circumvent ManifestV3 like this is yet another indication of its poor design.

Markus Seiffert

unread,
Sep 8, 2022, 7:39:51 AM9/8/22
to Chromium Extensions, wOxxOm, Markus Seiffert
>  It previously did in ManifestV2, which automatically excluded it from the CSP of the page
That's the main problem here. Now in MV3 i have to build a workaround for something that just worked in MV2.

I mean i would bundle my scripts into the extension too if the review process wouldn't take days. 
But if LinkedIn introduces some changes which in turn breaks our extension we do want to fix it right away and don't go through the whole review process again just because 2-3 lines of code changed.
I'm not exclusively talking about the Chrome Webstore, they're relatively fast most of the time but other stores (looking at you Microsoft) are extremely slow with their reviews.

I thought about having the main script that injects buttons / functionality inside the extension and just load some configuration (selectors / paths) as json from our server but that's by far not flexible enough.
So I'll probably use declaritiveNetRequests to edit the CSP of relevant pages for now.

hrg...@gmail.com

unread,
Sep 11, 2022, 6:04:57 PM9/11/22
to Chromium Extensions, net...@codedcomplexity.de, wOxxOm
Don't forget that Manifest V3 completely forbids remote code.

It's irrelevant that you can still execute remote code in MV3. Doing it is forbidden and therefore your extension will be removed from the store if you insist in doing it.

Sarsaparilla Sunset

unread,
Dec 19, 2022, 4:44:44 PM12/19/22
to Chromium Extensions, hrg...@gmail.com, net...@codedcomplexity.de, wOxxOm
So, to summarize, from content script context:
1) You cannot create a remote script tag due to extension CSP.
2) But you can create a local extension-bundled script tag.  This local script will run in the page context, which will be subject to the web page's CSP.
3) You can also call `scripting.executeScript({world: "MAIN"}) to do the same thing as (2).
4) If the page's CSP allows it, the script running in page context can then create a remote script tag that loads your remotely hosted code, albeit that would likely violate Chrome Web Store's policy.

I wonder:
1) Does allowing loading remote code from page context present a security risk?  My guess is yes.  A compromised server will allow bad actors to serve malicious code to extension users' computers that collect data, steal session tokens, popup ads or phish for credentials.
2) If I were a Chrome Dev, how would I go about closing this loophole?  Enforcing it after the fact is impractical.  There doesn't seem to be a "proper" way to do this that relies solely on the controls provided by CSPs and extension permissions, since fundamentally, code executed in page context falls outside of the extension's security purview.  Nobody likes hacks, but I think what has to be done is that extension-hosted scripts executed in page context will have to be subject to a modified page's CSP that forbids remote script-src's.

Carl Smith

unread,
Dec 19, 2022, 5:49:32 PM12/19/22
to Chromium Extensions, hai....@gmail.com, hrg...@gmail.com, net...@codedcomplexity.de, wOxxOm
You want to bypass the review process because it takes too long?? While that's understandable, it completely defeats the point of the review (and curated stores in general). In fact, your usecase is an example of exactly what MV3 aims to prevent.

Google should aim to speed up the review process, and look at other things they can do to get fixes to users quickly (maybe establish "trusted publisher" status that permits trusted devs to push changes now and be audited later), but MV3 should not permit extensions to basically opt out of having their code scrutinized.

I am sympathetic, but feel like you need to rethink your position a bit.
Reply all
Reply to author
Forward
0 new messages