Currently in the process of updating a Chrome extension to manifest v3 (curses upon you, Google monopoly!)
This extension is specifically designed for a particular website and includes numerous exciting features. To achieve this, I need to inject scripts that can access the page context. The website utilizes PageSpeed Insights (likely an older version as eval usage is questionable nowadays), which retrieves stringified code and evaluates it as shown below:
<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(){...}'` with subsequent evaluation -->
<script>eval(mod_pagespeed_nEcHBi$9_H);</script>
<script>eval(mod_pagespeed_gxZ81E5yX8);</script>
I devised a technique where I override the native eval function by gathering the evaluated string, creating a hash, and comparing it with stored hashes. If there's a match, I prevent the script from executing and insert my own instead. This method works well in theory and serves as a safety net in case the site updates its code.
However, the issue arises when approximately 50% of the time evaluation occurs before I override eval, often due to caching problems. A hard reset using Ctrl+Shift+R resolves the issue most of the time, allowing my scripts to take over as intended. But during normal reloads or site loads, the workaround fails to function.
manifest.json
...
"content_scripts": [{
"run_at": "document_start",
"js": ["js/content.js"],
"matches": [ "*://*.tanktrouble.com/*" ]
}
], ...
content.js (content script)
class GameLoader {
constructor() {
// Preload scripts, just because
this.hasherScript = Object.assign(document.createElement('script'), {
'src': chrome.runtime.getURL('js/hasher.js'),
'type': 'module' // Allows us to import utilities and data later
});
// Using custom elements as a workaround to incorporate my chrome runtime URL into the site via 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 used in hasherScript.js.
node.insertBefore(this.hasherScript, node.firstChild); // Injecting 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'; // Contains the earlier mentioned script hashes in the format { '7pqp95akl2s': 'game/gamemanager.js' ... }
import Hasher from '/js/utils/HashAlgorithm.js'; // Simple hash algorithm
/**
Implementation of the clever work-around for the extension URL.
*/
const nodeData = document.querySelector('tanktroubleaddons-url'),
extensionURL = nodeData.dataset.url;
window.t_url = function(url) {
return extensionURL + url;
}
// Modifying the native eval function and producing a hash of the evaluated script.
const proxied = eval;
const hashLength = ScriptHashes.length;
window.eval = function(code) {
if (typeof code === 'string') {
const codeHash = Hasher(code),
match = ScriptHashes.hashes[codeHash]; // Matching the new hash against existing hashes. If found, it provides a path for fetching the script.
if (match) {
// Synchronously fetching the script using jQuery. Upon completion, returning null to prevent execution of the original script.
$.getScript(t_url('js/injects/' + match), () => {
return null;
});
}
}
return proxied.apply(this, arguments);
}
Odd Behavior
Anomalies have been observed when switching hasherScript from a module to a regular script. When the type
parameter is excluded and the imports are directly included in hasher.js, everything loads smoothly and functions correctly. Scripts are evaluated consistently every time. It raises questions about a potential synchronous/asynchronous complication, though my search hasn't yielded any answers.
Many thanks in advance! :)