Inject javascript from content script with a chrome extension v3
Asked Answered
H

3

9

I'm migration my extension from V2 to V3. Now all is working fine except for one thing. In my V2 version I did

const actualCode = '(' + function () { 'console.log("demo");' } + `)();`;
const script = document.createElement('script');
script.textContent = actualCode;
(document.head || document.documentElement).appendChild(script);
script.remove();

Note that the console.log("demo") is a simplification of what I need to inject :)

I need to inject some javascript for my chrome-extension-magic to take place.

Now, in V3 this doesn't work anymore. I get the following error in my devtools-console

content.js:23114 
    
   Refused to execute inline script because it violates the following 
   ContentSecurity Policy directive: "script-src 'self'". Either the 
   'unsafe-inline' keyword, a hash ('sha256-tN52+5...6d2I/Szq8='), or a nonce
   ('nonce-...') is required to enable inline execution.

In the migration guide I noticed this section

"content_security_policy": {
   "extension_pages": "...",
   "sandbox": "..."
}

but there is not much description there, so this is magic to me. So I hope someone know can help me with this?

Hedva answered 24/12, 2021 at 16:21 Comment(11)
Use a separate file as shown in method 1 here. It runs asynchronously so it may run later than some scripts of the page. In the future chrome.scripting.registerContentScripts will allow specifying world.Niello
Thats it, thnx. I've tested it, but in my case this solution doesn't work unfortunately. In my case I need the injected script to run before the page's scripts runs. I've tested this solution and noticed that the injected script now runs too late :(Hedva
You'll have to keep using MV2.Niello
Yes, I came to the exact same conclusionHedva
I see you use it to override XHR/fetch so here's an alternative (in case the site doesn't use the deprecated synchronous XHR): override XMLHttpRequest.prototype.response getter (also responseText) and Response.prototype.text getter (also json, blob, arrayBuffer, formData) via Object.getOwnPropertyDescriptor + Object.defineProperty. These getters are used after the remote server responds so your script should always run earlier.Niello
Thats a very interesting suggestion, and might be the solution to my probleem. I will investigate, because it should also work, for example, if the endpoint being called not yet even exists! Thnx for the suggestion!Hedva
Do you think it would still be possible, if you consider the fact that I have to retrieve the response from chrome.strorage (async). It means that to be able to set a value for responseText I need to dispatch a message to the content script, asking for the data.Hedva
Since chrome.storage is always queried locally (even for sync storage) it should be much faster than a remote request by the site so unless there's a bug in Chrome it should be fine.Niello
True, but if I replace XMLHttpRequest.prototype.response with my own getter, now as soon my getter is called, it means the server's response has already arrived and it will be to late for me to make that chrome.storage callHedva
This is where MCVE is necessary. There may be a solution depending on what the actual workflow and the actual data are e.g. querying the [entire] storage when the content script starts.Niello
yes, all this is also way off topic :) Thanks for the help!Hedva
P
17

Refer to Access variables and functions defined in page context from an extension

Since content scripts are executed in an "isolated world" environment, we can't do some special dom operations in content_script js.

This example will show you how to inject injected-script.js to web page before "document start":

content-script.js

function injectScript (src) {
    const s = document.createElement('script');
    s.src = chrome.runtime.getURL(src);
    s.onload = () => s.remove();
    (document.head || document.documentElement).append(s);
}

injectScript('injected-script.js')

manifest.json (Manifest V3)

"content_scripts": [
    {
        "matches": ["<all_urls>"],
        "js": ["content-script.js"],
        "run_at": "document_start" // <-- Instead of the default "document_end"
    }
],
"web_accessible_resources": [{
    "resources": ["injected-script.js"], // <-- Don't forget to add this
    "matches": ["<all_urls>"]
}]

Updated: 2023-03-08

To support es6 module imports, just add s.type = "module"

content-script.js

function injectScript (src) {
    const s = document.createElement('script');
    s.src = chrome.runtime.getURL(src);
    s.type = "module" // <-- Add this line for ESM module support
    s.onload = () => s.remove();
    (document.head || document.documentElement).append(s);
}

injectScript('injected-script.js')

Then you could use the es6 import statement like this:

injected-script.js

import { doStuff } from './my-other-script.js';

doStuff();
Proboscis answered 12/2, 2022 at 16:52 Comment(5)
do we need to add any csp line/directive for this appendChild?Shaw
@Shaw We should put the path “intject.js” to "web_accessible_resources" lik my example.Proboscis
Dont we need csp policy? Why about if we are injecting inline js code?Shaw
I have this working to inject the script, I can see the tag however objects that are defined in my script are not defined. Any ideasCynarra
This saves me, thanks very much. I was migrating from v2 to v3, originally we are using injected script directly as script.textContent = content, in v3 this no longer works, changed to script.src = chrome.runtime.getURL('test.js');Howlyn
P
0

There's a safer way to inject JS instead of using script which is dynamic import. It'd be something like this below:

(async () => {
  const src = chrome.extension.getURL('src/js/main.js');
  const contentScript = await import(src);
  contentScript.main();
})();
Prevention answered 31/8, 2023 at 19:33 Comment(0)
S
-1

How about using the chrome.scripting API.

From the linked docs,

{
  "permissions": ["scripting", "activeTab"]
}

Inject into all tabs,

function getTabId() { ... }

chrome.scripting
    .executeScript({
      target : {tabId : getTabId(), allFrames : true},
      files : [ "script.js" ],
    })
    .then(() => console.log("script injected in all frames"));
Surge answered 25/2 at 16:2 Comment(1)
This is a copy paste from some of the worst documentation Google have supplied where they've just omitted the required function code for getTabidCardiograph

© 2022 - 2024 — McMap. All rights reserved.