48

Is it possible to inject JS before page load, or is it necessary to use content scripts and way for the document to finish?

For example, is there a faster way to execute JS that turns the page red as soon as it's opened?

Ian McIntyre Silber
  • 5,553
  • 13
  • 53
  • 76

2 Answers2

69

Declare a content script in the manifest file with "run_at": "document_start" to get it to run as soon as possible, i.e. right after constructing the document root (when <head> does not exist yet).

For your very specific example, it might be better to declare a content style instead, similar to content scripts, but using the "css" key instead of "js".

If you want to dynamically run a script as soon as possible, then call chrome.tabs.executeScript when the chrome.webNavigation.onCommitted event is triggered.

Theblockbuster1
  • 308
  • 6
  • 15
Rob W
  • 341,306
  • 83
  • 791
  • 678
  • Special thanks for the reference to `chrome.webNavigation.onCommitted`, much handier than `chrome.tabs.onUpdated`. – cprcrack May 17 '17 at 13:32
  • 1
    @RobW, onComitted runs when "*at least part of the document has been received from the server*". How do we get the moment before that then? The moment right when the user click the address bar and click key [--eg when he's reloading the page, or loading a new one]. – Pacerier Aug 07 '17 at 22:11
  • 2
    @RobW, Separately, the doc writes "*If a navigation was triggered via Chrome Instant or Instant Pages, a completely loaded page is swapped into the current tab*". In such a case, is it still possible to run a script to replace the webpage's `window` object with my own proxy win object right before any webpage code could even run? – Pacerier Aug 07 '17 at 22:17
  • 2
    Btw manifest's document_start contentscript will run before `chrome.webNavigation.onCommitted`, but it has no access to chrome functions available to bg. For bg, `chrome.webNavigation.onCommitted` is still the earliest way. – Pacerier Aug 08 '17 at 04:25
  • 1
    With the executeScript API, there is no guarantee that your script runs in time. If this is important, declare a script in manifest.json – Rob W Aug 08 '17 at 08:58
  • Btw I made some tests for a https page https://pastebin.com/gJMDQw9P, and a http page https://pastebin.com/Yb2GSNzW. All code tested on a 60.0.3112.78 (Official Build) (64-bit), Mac OS X, V8 6.0.286.44. (I didn't test on ftp and file pages.) I found a **chrome bug** whereby manifest cs is sometimes not run. This bug is found not on a httpS page, but on a http page. Without manifest cs and webrequests callbacks running, the earliest point in time is and its too late by... – Pacerier Aug 10 '17 at 02:37
  • 1
    ...then, because if I wanted to replace `window` of the page with a proxy, the website could have already obtained a reference to the original `window` object before I managed to replace it. – Pacerier Aug 10 '17 at 02:38
  • As for the bg page, my tests above show that it's not true that the earliest point in time is `chrome.webNavigation.onCommitted`. **~~~** The httpS tests show that while nv onBeforeNavigate and r onBeforeRequest occurs before the page, the earliest point in time after the page may have been either r onBeforeSendHeaders, r onSendHeaders, r onHeadersReceived, r onResponseStarted, r onCompleted, or . **~~~** The http tests show that sometimes the earliest point may even be nv onBeforeNavigate or r onBeforeRequest. – Pacerier Aug 10 '17 at 02:49
  • Hmm, as for my comment on https://stackoverflow.com/questions/19191679/chrome-extension-inject-js-before-page-load/19192010#comment78166411_19192010, perhaps webrequests would not have been ignored had they been **blocking** webrequests. Need to test. – Pacerier Aug 10 '17 at 04:42
  • And also, have manifest js set may more often show cases where `executeScript` works for the events before `chrome.webNavigation.onCommitted`. – Pacerier Sep 11 '17 at 22:24
  • Btw, is manifest js document_start **guaranteed** to run at start? Or is it at an [at-best](http://archive.is/bgTJ8#selection-11063.5-11063.12) basis like executeScript? – Pacerier Sep 11 '17 at 22:32
  • 1
    @RobW, Btw I tried this script `document.documentElement.appendChild(document.createElement('script')).innerHTML = 'window.open=null;document.createElement=null'` in manifest js doc start, which works.. but `document.documentElement.appendChild(document.createElement('script')).innerHTML = 'document=null;window=null'` doesn't. Do you know how can `window` and `document` be overridden? – Pacerier Sep 11 '17 at 22:50
  • 1
    Ref https://stackoverflow.com/questions/12095924/is-it-possible-to-inject-a-javascript-code-that-overrides-the-one-existing-in-a/12096099#comment80379957_12096099 – Pacerier Oct 12 '17 at 18:55
  • Links are broken (404) and `chrome.webNavigation` is undefined – dustytrash Jan 15 '21 at 01:40
  • Sadly no longer possible with manifest V3, it's replaced by `scripting.executeScript [0]`: "Injects a script into a target context. The script will be run at document_idle [1]" [0]https://developer.chrome.com/docs/extensions/reference/scripting/#method-executeScript [1] https://stackoverflow.com/questions/33248629/when-does-a-run-at-document-idle-content-script-run – Maciej Krawczyk Jun 26 '23 at 08:05
2

For Manifest V3 there was a new field added to content scripts in 102.0.5005.58 Stable: world

And there are also a lot of chrome bugs related to that topic: #634381, #1054624, #1207006

You have to use "world": "main" at the content script together with "run_at": "document_start" and a CSP to allow the injection from the Extension. Otherwise the injected script gets rejected with:

Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self' 'wasm-unsafe-eval'". Either the 'unsafe-inline' keyword, a hash ('sha256-*'), or a nonce ('nonce-...') is required to enable inline execution.

"world": "MAIN":

[Extensions] Add main world injections for dynamic content scripts

This CL adds a new field "world" for dynamic content scripts which allows the extension to specify if the script will run in the isolated or main world. By default, scripts which do not specify this field will run in the isolated world.

Valid attributes are "ISOLATED" (default) or "MAIN".

Example files i used:

manifest.json

{
  "name": "script injection",
  "version": "0",
  "manifest_version": 3,
  "minimum_chrome_version": "103.0",
  "content_scripts": [
    {
      "matches": ["*://*/*"],
      "js": ["inject.js"],
      "run_at": "document_start",
      "world": "MAIN"
    }
  ],
  "web_accessible_resources": [{
    "resources": ["injected.js"],
    "matches": ["<all_urls>"]
  }],
  "content_security_policy": {
    "extension_pages": "default-src 'self' 'wasm-unsafe-eval';"
  }
}

inject.js

let el = document.createElement("script");
el.src = chrome.runtime.getURL("injected.js");
document.documentElement.appendChild(el);
console.log("injected");

injected.js

console.log(typeof alert); // "function"
console.log("injected file");
delete window.alert;
console.log(typeof alert); // "undefined"

[enter image description here]

Christopher
  • 3,124
  • 2
  • 12
  • 29
  • Thank you for your answer! I'm using `"world": "MAIN"` though, and if the page is cached, on simple refresh my script does not run early enough. My code is almost identical to yours though, and if yours is working, I'll keep poking at mine to see if I can figure out what the difference is. – Brad Jan 22 '23 at 19:36
  • @Brad I tried it with several sites and even local files. The script was always injected directly. I tested it on two machines (Ryzon 9) and a slow laptop (Celeron N4020). - Reloading and navigating through cached sites backward/forward worked fine too. `alert` was always unavailable.- Both machines mentioned are currently on chrome 109.0.5414.75 Stabl – Christopher Jan 22 '23 at 20:08
  • Hi Christopher, thank you for your comments and tests. I'm still having trouble ensuring my script runs first. To demonstrate, here's the code I'd normally put in `injected.js`: `Object.defineProperty(navigator.mediaDevices, 'getUserMedia', { get: (...args) => { console.log('Intercepted!'); } });` And then, I'm testing with this URL: https://webrtc.github.io/samples/src/content/getusermedia/audio/ On first load, it works (I see "intercepted" on the console). If I click refresh, it doesn't work. If I Ctrl+Shift Refresh, it works. If I disable cache, it works. – Brad Jan 27 '23 at 18:24
  • Also, I've tried setting `el.async = false;`, but it made no difference as far as I could see. – Brad Jan 27 '23 at 18:27
  • @Brad thanks for the reply. How about raising a new [issue for chromium](https://bugs.chromium.org/p/chromium/issues/list)? I've tried it with your example too and it looks like the script gets implemented at the right spot but the execution seems to be out of place on a basic refresh. If you add an additional log line to the injected.js, it gets always executed (no matter if F5 or CTRL+F5) but the hook/override doesn't seem to apply correctly on plain F5. – Christopher Jan 27 '23 at 18:44
  • In reading through the bug reports again, I'm thinking it has something to do with the last comment here: https://bugs.chromium.org/p/chromium/issues/detail?id=1207006#c12 "The new field 'world' only works for `chrome.scripting.registerContentScripts`..." In theory, if I could get the `WORLD` behavior to work, I wouldn't need to inject anything. I'm going to dig at that a bit... so far I've just used content scripts from the manifest. – Brad Jan 27 '23 at 18:53
  • 1
    More specific bug here, for anyone looking for the issue: https://bugs.chromium.org/p/chromium/issues/detail?id=634381 – Brad Jan 27 '23 at 20:47
  • So pretty much it's impossible to do it for any arbitrary website due to CSP? – Maciej Krawczyk Jun 26 '23 at 08:07