I'm working on rewriting a Chrome extension to manifest v3 (damn you, Google monopoly!)
It's targeted towards a certain website, adding a bunch of cool features. This means I have to inject script that can access the page context. The website uses PageSpeed Insights (presumably an older version because I doubt they use eval nowadays), which fetches stringified code and evaluates it as such:
<script src="https://cdn.tanktrouble.com/RELEASE-2021-07-06-01/js/red_infiltration.js+messages.js.pagespeed.jc.vNQfCE2Wzx.js"></script>
<!-- ^ Contains the variables `mod_pagespeed_nEcHBi$9_H = 'function foo(){...}'` and `mod_pagespeed_gxZ81E5yX8 = 'function bar(){...}'` and the scripts below evaluate them -->
<script>eval(mod_pagespeed_nEcHBi$9_H);</script>
<script>eval(mod_pagespeed_gxZ81E5yX8);</script>
Now, I've come up with a method where I override the native eval function, take the string that it being evaluated, I generate a hash and compare it with some stored hashes. If they match, I stop the script from being evaluated and inject mine instead. Works good in theory, and acts as a failsafe if the site updates any code.
The problem lies in the fact that a good 50% of the time, the evaluation happens before I override eval, and it seems to relate to cache. If I do a hard reset (Ctrl+Shift+R) it works most of the time and injects my scripts instead as intended. However, a normal reload/site load, and it won't work.
manifest.json
...
"content_scripts": [{
"run_at": "document_start",
"js": ["js/content.js"],
"matches": [ "*://*.tanktrouble.com/*" ]
}
], ...
content.js (content script)
class GameLoader {
constructor() {
// Preload scripts, why not?
this.hasherScript = Object.assign(document.createElement('script'), {
'src': chrome.runtime.getURL('js/hasher.js'),
'type': 'module' // So we can import some utils and data later on
});
// These custom elements are a hacky method to get my chrome runtime URL into the site with element datasets.
this.extensionURL = document.createElement('tanktroubleaddons-url');
this.extensionURL.dataset.url = chrome.runtime.getURL('');
}
observe() {
return new Promise(resolve => {
const observer = new MutationObserver((mutations, observer) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node.tagName === 'HEAD') {
node.insertBefore(this.extensionURL, node.firstChild); // extensionURL is used in hasherScript.js.
node.insertBefore(this.hasherScript, node.firstChild); // Inject the hash module
resolve(), observer.disconnect();
}
}
}
})
.observe(document.documentElement, { childList: true, subtree: true });
});
}
}
const loader = new GameLoader();
loader.observe();
hasher.js (this.hasherScript)
import ScriptHashes from '/config/ScriptHashes.js'; // The script hashes previously mentioned. The format is { '7pqp95akl2s': 'game/gamemanager.js' ... }
import Hasher from '/js/utils/HashAlgorithm.js'; // Quick hash algorithm
/**
Here we implement the aforementioned hacky method for the extension URL.
*/
const nodeData = document.querySelector('tanktroubleaddons-url'),
extensionURL = nodeData.dataset.url;
window.t_url = function(url) {
return extensionURL + url;
}
// Change the native eval function and generate a hash of the script being evaluated.
const proxied = eval;
const hashLength = ScriptHashes.length;
window.eval = function(code) {
if (typeof code === 'string') {
const codeHash = Hasher(code),
match = ScriptHashes.hashes[codeHash]; // Check the newly generated hash against my list of hashes. If match, it should return a path to use be used in script fetching.
if (match) {
// Synchronous script fetching with jQuery. When done, return null so original script won't be evaluated.
$.getScript(t_url('js/injects/' + match), () => {
return null;
});
}
}
return proxied.apply(this, arguments);
}
Weird behaviour
I have noticed weird behaviour when changing hasherScript to a regular script rather than a module. If I exclude the type
paramter and paste the imports directly into hasher.js, things load and work perfectly. Scripts get evaluated every time et cetera. It makes me wonder if it's a synchronous/asynchronous issue.
I haven't been able to find anything on this, unfortunately.
Thanks in advance! :)