use crate::manifest::Manifest;
pub fn generate_bootstrap(manifest: &Manifest) -> String {
let mut js = String::with_capacity(4096);
js.push_str(CHROME_NAMESPACE);
if manifest.permissions.contains(&"tabs".into())
|| manifest.host_permissions.iter().any(|h| h.contains("*"))
{
js.push_str(CHROME_TABS);
}
if manifest.permissions.contains(&"cookies".into()) {
js.push_str(CHROME_COOKIES);
}
if manifest.permissions.contains(&"storage".into()) {
js.push_str(CHROME_STORAGE);
}
if manifest.permissions.contains(&"webRequest".into())
|| manifest.permissions.contains(&"webRequestBlocking".into())
{
js.push_str(CHROME_WEB_REQUEST);
}
if manifest.permissions.contains(&"alarms".into()) {
js.push_str(CHROME_ALARMS);
}
if manifest.permissions.contains(&"identity".into()) {
js.push_str(CHROME_IDENTITY);
}
if manifest.permissions.contains(&"bookmarks".into()) {
js.push_str(CHROME_BOOKMARKS);
}
if manifest.permissions.contains(&"history".into()) {
js.push_str(CHROME_HISTORY);
}
if manifest.permissions.contains(&"downloads".into()) {
js.push_str(CHROME_DOWNLOADS);
}
if manifest.permissions.contains(&"notifications".into()) {
js.push_str(CHROME_NOTIFICATIONS);
}
js.push_str(CHROME_RUNTIME);
js.push_str(CHROME_SCRIPTING);
js.push_str(CHROME_PERMISSIONS);
js.push_str(CHROME_ACTION);
js.push_str(CHROME_WINDOWS);
js.push_str(CHROME_CONTEXT_MENUS);
js.push_str(CHROME_I18N);
js
}
const CHROME_NAMESPACE: &str = r#"
// jsdet Chrome Extension bootstrap
var chrome = chrome || {};
// ─── Web API polyfills needed by webpack bundles ────────────────
// These prevent ReferenceErrors when extension code accesses standard globals.
// QuickJS has most of these but service workers expect them on globalThis.
// Virtual clock — advances during setTimeout to defeat timing-based evasion.
// Real browsers: Date.now() increases between setTimeout callbacks.
// Without this, malware detects the sandbox via `elapsed < 2`.
var __virtualTimeOffset = 0;
var __origDateNow = Date.now;
Date.now = function() { return __origDateNow.call(Date) + __virtualTimeOffset; };
if (typeof performance !== 'undefined' && performance.now) {
var __origPerfNow = performance.now.bind(performance);
performance.now = function() { return __origPerfNow() + __virtualTimeOffset; };
}
// Timer APIs — execute callbacks synchronously (no real async in sandbox)
if (typeof setTimeout === 'undefined') {
var __timerId = 0;
function setTimeout(fn, ms) {
__timerId++;
// Advance virtual clock by the delay amount — simulates real timing.
// Advance by at least 4ms (Chrome's minimum timer resolution).
// setTimeout(fn, 0) still takes ~4ms in a real browser.
__virtualTimeOffset += Math.max(ms || 0, 4);
if (typeof fn === 'function') {
fn();
} else if (typeof fn === 'string') {
// setTimeout("code", delay) is equivalent to eval("code")
// This is a known attack vector — detect it as code execution
__jsdet_call('eval', [fn]);
if (typeof __jsdet_check_taint_at_sink === 'function') {
__jsdet_check_taint_at_sink('setTimeout', fn);
}
eval(fn);
}
return __timerId;
}
function setInterval(fn, ms) {
__timerId++;
if (typeof fn === 'string') {
__jsdet_call('eval', [fn]);
if (typeof __jsdet_check_taint_at_sink === 'function') {
__jsdet_check_taint_at_sink('setInterval', fn);
}
} else if (typeof fn === 'function') {
fn();
}
return __timerId;
}
function clearTimeout(id) {}
function clearInterval(id) {}
}
// Console — no-op but prevents crashes
if (typeof console === 'undefined') {
var console = {
log: function() {},
warn: function() {},
error: function() {},
info: function() {},
debug: function() {},
trace: function() {},
assert: function() {},
dir: function() {},
table: function() {},
group: function() {},
groupEnd: function() {},
time: function() {},
timeEnd: function() {},
};
}
// URL constructor — needed for URL parsing in extensions
if (typeof URL === 'undefined') {
function URL(url, base) {
if (base && url.indexOf('://') === -1) {
url = base.replace(/\/[^\/]*$/, '/') + url;
}
this.href = url;
var match = url.match(/^(\w+):\/\/([^\/\?#]+)(\/[^?#]*)?(\?[^#]*)?(#.*)?/);
this.protocol = match ? match[1] + ':' : '';
this.hostname = match ? match[2].split(':')[0] : '';
this.host = match ? match[2] : '';
this.port = match ? (match[2].split(':')[1] || '') : '';
this.pathname = match ? (match[3] || '/') : '/';
this.search = match ? (match[4] || '') : '';
this.hash = match ? (match[5] || '') : '';
this.origin = this.protocol + '//' + this.host;
this.searchParams = {
get: function(k) { var m = (this._s||'').match(new RegExp('[?&]'+k+'=([^&]*)')); return m ? decodeURIComponent(m[1]) : null; }.bind({_s: this.search}),
has: function(k) { return (this.search || '').indexOf(k + '=') >= 0; }.bind(this),
toString: function() { return this.search.substring(1); }.bind(this),
};
this.toString = function() { return this.href; };
}
}
// TextEncoder/TextDecoder — needed for binary data handling
if (typeof TextEncoder === 'undefined') {
function TextEncoder() {}
TextEncoder.prototype.encode = function(str) {
var arr = [];
for (var i = 0; i < str.length; i++) {
var c = str.charCodeAt(i);
if (c < 128) arr.push(c);
else if (c < 2048) { arr.push(192 | (c >> 6)); arr.push(128 | (c & 63)); }
else { arr.push(224 | (c >> 12)); arr.push(128 | ((c >> 6) & 63)); arr.push(128 | (c & 63)); }
}
return new Uint8Array(arr);
};
}
if (typeof TextDecoder === 'undefined') {
function TextDecoder() {}
TextDecoder.prototype.decode = function(bytes) {
var str = '';
for (var i = 0; i < bytes.length; i++) {
if (bytes[i] < 128) str += String.fromCharCode(bytes[i]);
else if (bytes[i] < 224) { str += String.fromCharCode(((bytes[i] & 31) << 6) | (bytes[++i] & 63)); }
else { str += String.fromCharCode(((bytes[i] & 15) << 12) | ((bytes[++i] & 63) << 6) | (bytes[++i] & 63)); }
}
return str;
};
}
// crypto.getRandomValues — needed for UUID generation in extensions
if (typeof crypto === 'undefined') {
var crypto = {
getRandomValues: function(arr) {
for (var i = 0; i < arr.length; i++) arr[i] = (Math.random() * 256) | 0;
return arr;
},
subtle: {
digest: function() { return {then: function(cb) { if(cb) cb(new ArrayBuffer(32)); return this; }}; },
encrypt: function() { return {then: function(cb) { return this; }}; },
decrypt: function() { return {then: function(cb) { return this; }}; },
generateKey: function() { return {then: function(cb) { return this; }}; },
importKey: function() { return {then: function(cb) { return this; }}; },
exportKey: function() { return {then: function(cb) { return this; }}; },
},
randomUUID: function() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random()*16|0; return (c==='x'?r:(r&3|8)).toString(16); }); },
};
}
// FormData — needed for multipart uploads
if (typeof FormData === 'undefined') {
function FormData() { this._data = []; }
FormData.prototype.append = function(k, v) { this._data.push([k, v]); };
FormData.prototype.get = function(k) { for (var i=0;i<this._data.length;i++) if(this._data[i][0]===k) return this._data[i][1]; return null; };
}
// Response/Request/Headers — needed for service worker fetch
if (typeof Response === 'undefined') {
function Response(body, init) { this.body = body; this.status = (init&&init.status)||200; this.ok = this.status < 400; }
Response.prototype.json = function() { var b=this.body; return {then:function(cb){if(cb)cb(JSON.parse(b));return this;}}; };
Response.prototype.text = function() { var b=this.body; return {then:function(cb){if(cb)cb(String(b));return this;}}; };
}
if (typeof Request === 'undefined') {
function Request(url, init) { this.url = url; this.method = (init&&init.method)||'GET'; }
}
if (typeof Headers === 'undefined') {
function Headers(init) { this._h = init || {}; }
Headers.prototype.get = function(k) { return this._h[k.toLowerCase()] || null; };
Headers.prototype.set = function(k, v) { this._h[k.toLowerCase()] = v; };
}
// Global addEventListener — captures postMessage handlers.
// Many extensions/pages call addEventListener("message", handler) at top level.
var __message_listeners = [];
if (typeof addEventListener === 'undefined') {
function addEventListener(type, fn) {
if (type === 'message' && typeof fn === 'function') {
__message_listeners.push(fn);
// Also register with chrome.runtime.onMessage for probing
if (chrome && chrome.runtime && chrome.runtime.onMessage) {
chrome.runtime.onMessage._listeners.push(function(msg, sender, sendResponse) {
// Wrap as a MessageEvent-like object
try { fn({data: msg, origin: 'https://attacker.com', source: null}); } catch(e) {}
});
}
}
}
}
function removeEventListener() {}
// WeakRef polyfill — prevents sandbox detection via typeof WeakRef === 'undefined'
if (typeof WeakRef === 'undefined') {
var WeakRef = function(target) { this._target = target; };
WeakRef.prototype.deref = function() { return this._target; };
if (typeof globalThis !== 'undefined') globalThis.WeakRef = WeakRef;
}
// FinalizationRegistry polyfill — prevents sandbox detection via typeof check
if (typeof FinalizationRegistry === 'undefined') {
var FinalizationRegistry = function(callback) { this._cb = callback; };
FinalizationRegistry.prototype.register = function(target, heldValue, unregisterToken) {};
FinalizationRegistry.prototype.unregister = function(token) {};
if (typeof globalThis !== 'undefined') globalThis.FinalizationRegistry = FinalizationRegistry;
}
// OffscreenCanvas + WebGL polyfill — prevents sandbox detection via canvas.getContext('webgl')
if (typeof OffscreenCanvas === 'undefined') {
var OffscreenCanvas = function(w, h) { this.width = w; this.height = h; };
OffscreenCanvas.prototype.getContext = function(type) {
if (type === 'webgl' || type === 'webgl2' || type === 'experimental-webgl') {
return {
getParameter: function(p) { return p === 7937 ? 'WebKit' : 'OpenGL ES 3.0'; },
getExtension: function() { return {}; },
createShader: function() { return {}; },
createProgram: function() { return {}; },
getSupportedExtensions: function() { return ['WEBGL_debug_renderer_info']; },
};
}
return { fillRect: function(){}, clearRect: function(){}, getImageData: function(){ return {data:[0,0,0,255]}; } };
};
if (typeof globalThis !== 'undefined') globalThis.OffscreenCanvas = OffscreenCanvas;
}
// HTMLCanvasElement polyfill for document.createElement('canvas').getContext
if (typeof HTMLCanvasElement === 'undefined') {
var HTMLCanvasElement = function() {};
HTMLCanvasElement.prototype.getContext = OffscreenCanvas.prototype.getContext;
}
// MutationObserver / IntersectionObserver / ResizeObserver — DOM observers
if (typeof MutationObserver === 'undefined') {
function MutationObserver(cb) {} MutationObserver.prototype.observe = function(){}; MutationObserver.prototype.disconnect = function(){};
}
if (typeof IntersectionObserver === 'undefined') {
function IntersectionObserver(cb) {} IntersectionObserver.prototype.observe = function(){}; IntersectionObserver.prototype.disconnect = function(){};
}
if (typeof ResizeObserver === 'undefined') {
function ResizeObserver(cb) {} ResizeObserver.prototype.observe = function(){}; ResizeObserver.prototype.disconnect = function(){};
}
// ─── Anti-evasion: browser API polyfills ────────────────────────
// Extensions check for these APIs to detect sandboxes. Each polyfill
// prevents one fingerprinting technique. Added carefully — each one
// tested to not break the eval/fetch/Function overrides above.
if (typeof AudioContext === 'undefined') {
var AudioContext = function() { this.state = 'running'; };
AudioContext.prototype.createOscillator = function() { return {connect:function(){},start:function(){},stop:function(){}}; };
AudioContext.prototype.close = function() {};
var webkitAudioContext = AudioContext;
}
if (typeof Intl === 'undefined') {
var Intl = {
DateTimeFormat: function() { this.format = function(d){return String(d);}; this.resolvedOptions = function(){return {timeZone:'America/New_York'};}; },
NumberFormat: function() { this.format = function(n){return String(n);}; },
};
}
if (typeof WebAssembly === 'undefined') {
var WebAssembly = {
compile: function(bytes) {
// CRITICAL FIX: Track taint through WebAssembly.compile
// If bytes come from a tainted source, the compiled module is tainted
var is_tainted = false;
if (bytes && typeof __jsdet_get_taint === 'function') {
// Check if bytes.buffer exists (TypedArray) or bytes is ArrayBuffer
var buf = bytes.buffer || bytes;
if (buf.__jsdet_tainted) {
is_tainted = true;
}
}
__jsdet_bridge_call("WebAssembly.compile", JSON.stringify([bytes ? bytes.length : 0, is_tainted]));
if (is_tainted && typeof __jsdet_check_taint_at_sink === 'function') {
__jsdet_check_taint_at_sink('WebAssembly.compile', '<tainted wasm bytes>');
}
var module = {};
module.__jsdet_tainted = is_tainted;
module.__jsdet_wasm_bytes = bytes;
return Promise.resolve(module);
},
instantiate: function(bytes, imports) {
// CRITICAL FIX: Track taint through WebAssembly.instantiate
var is_tainted = false;
if (bytes && typeof __jsdet_get_taint === 'function') {
var buf = bytes.buffer || bytes;
if (buf.__jsdet_tainted) {
is_tainted = true;
}
}
// Also check if bytes is a Module object with taint marker
if (bytes && bytes.__jsdet_tainted) {
is_tainted = true;
}
__jsdet_bridge_call("WebAssembly.instantiate", JSON.stringify([bytes ? bytes.length : 0, is_tainted]));
if (is_tainted && typeof __jsdet_check_taint_at_sink === 'function') {
__jsdet_check_taint_at_sink('WebAssembly.instantiate', '<tainted wasm module>');
}
var wrappedImports = {};
if (imports) {
for (var mod in imports) {
wrappedImports[mod] = {};
for (var key in imports[mod]) {
var val = imports[mod][key];
// CRITICAL FIX: Track taint through import type conversions
if (typeof val === 'function') {
wrappedImports[mod][key] = (function(orig, m, k) {
return function() {
var has_tainted_arg = false;
for (var i = 0; i < arguments.length; i++) {
// Check taint on strings
if (typeof arguments[i] === 'string' && typeof __jsdet_get_taint === 'function') {
var label = __jsdet_get_taint(arguments[i]);
if (label > 0) {
__jsdet_check_taint_at_sink("wasm_import_" + m + "." + k, arguments[i]);
has_tainted_arg = true;
}
}
// CRITICAL FIX: Check taint on numbers (type conversion from string)
if (typeof arguments[i] === 'number' && typeof __jsdet_get_taint === 'function') {
// Numbers themselves can't carry taint, but if they came from
// tainted strings via type conversion, we need to track that
var numStr = String(arguments[i]);
var label = __jsdet_get_taint(numStr);
if (label > 0) {
__jsdet_check_taint_at_sink("wasm_import_number_" + m + "." + k, numStr);
has_tainted_arg = true;
}
}
}
var result = orig.apply(this, arguments);
// CRITICAL FIX: Propagate taint to return value if args were tainted
if (has_tainted_arg && typeof result === 'string' && typeof __jsdet_set_taint === 'function') {
__jsdet_set_taint(result, 1);
}
return result;
};
})(val, mod, key);
} else {
wrappedImports[mod][key] = val;
}
}
}
}
// Mock WASM instance with taint tracking
var mem = new ArrayBuffer(65536);
mem.__jsdet_tainted = is_tainted; // Propagate taint to memory
var mockInstance = {
exports: {
memory: mem,
mock_export: function(arg) {
if (wrappedImports.env && typeof wrappedImports.env.mock_import === 'function') {
wrappedImports.env.mock_import(arg);
}
return arg;
}
}
};
var wrappedExports = {};
for (var exp in mockInstance.exports) {
var val = mockInstance.exports[exp];
if (typeof val === 'function') {
wrappedExports[exp] = (function(orig, e) {
return function() {
var has_tainted_arg = false;
for (var i = 0; i < arguments.length; i++) {
// CRITICAL FIX: Check taint on all argument types
if (typeof arguments[i] === 'string' && typeof __jsdet_get_taint === 'function') {
var label = __jsdet_get_taint(arguments[i]);
if (label > 0) {
__jsdet_check_taint_at_sink("wasm_export_" + e, arguments[i]);
has_tainted_arg = true;
}
}
// CRITICAL FIX: Check taint on numbers (type conversion)
if (typeof arguments[i] === 'number' && typeof __jsdet_get_taint === 'function') {
var numStr = String(arguments[i]);
var label = __jsdet_get_taint(numStr);
if (label > 0) {
__jsdet_check_taint_at_sink("wasm_export_number_" + e, numStr);
has_tainted_arg = true;
}
}
}
var result = orig.apply(this, arguments);
// CRITICAL FIX: Propagate taint to return value
if (typeof result === 'string' && typeof __jsdet_set_taint === 'function') {
if (has_tainted_arg || is_tainted) {
__jsdet_set_taint(result, 1);
}
}
return result;
};
})(val, exp);
} else {
wrappedExports[exp] = val;
}
}
mockInstance.exports = wrappedExports;
var resultModule = {};
resultModule.__jsdet_tainted = is_tainted;
return Promise.resolve({ instance: mockInstance, module: resultModule });
},
// CRITICAL FIX: Track taint through Module constructor
Module: function(bytes) {
var is_tainted = false;
if (bytes && typeof __jsdet_get_taint === 'function') {
var buf = bytes.buffer || bytes;
if (buf.__jsdet_tainted) {
is_tainted = true;
}
}
this.__jsdet_tainted = is_tainted;
this.__jsdet_wasm_bytes = bytes;
__jsdet_bridge_call("WebAssembly.Module", JSON.stringify([bytes ? bytes.length : 0, is_tainted]));
if (is_tainted && typeof __jsdet_check_taint_at_sink === 'function') {
__jsdet_check_taint_at_sink('WebAssembly.Module', '<tainted wasm bytes>');
}
},
Instance: function(module, imports) {
this.exports = {};
// Inherit taint from module
if (module && module.__jsdet_tainted) {
this.__jsdet_tainted = true;
}
},
Memory: function(desc) {
var buf = new ArrayBuffer((desc && desc.initial || 1) * 65536);
return { buffer: buf };
},
Table: function() { return { get: function() { return null; } }; },
validate: function(bytes) {
// CRITICAL FIX: Even validation can leak info via timing
// Track that validation was attempted
__jsdet_bridge_call("WebAssembly.validate", JSON.stringify([bytes ? bytes.length : 0]));
return true;
},
};
}
if (typeof indexedDB === 'undefined') {
var indexedDB = { open: function(){return {onsuccess:null,onerror:null};} };
}
if (typeof Notification === 'undefined') {
var Notification = function(t){this.title=t;};
Notification.permission = 'granted';
Notification.requestPermission = function(cb){if(cb)cb('granted');};
}
if (typeof WebGLRenderingContext === 'undefined') {
var WebGLRenderingContext = function() {};
}
// requestAnimationFrame — needed by extensions that check for DOM animation support
if (typeof requestAnimationFrame === 'undefined') {
var requestAnimationFrame = function(cb) { __virtualTimeOffset += 16; cb(Date.now()); return 1; };
var cancelAnimationFrame = function() {};
if (typeof globalThis !== 'undefined') { globalThis.requestAnimationFrame = requestAnimationFrame; globalThis.cancelAnimationFrame = cancelAnimationFrame; }
}
// Screen — non-zero dimensions defeat screen-based sandbox detection.
// Use try/defineProperty to avoid shadowing issues.
try {
if (typeof screen === 'undefined' || (typeof screen === 'object' && screen.width === 0)) {
globalThis.screen = {width:1920,height:1080,availWidth:1920,availHeight:1040,colorDepth:24,pixelDepth:24};
}
} catch(e) {}
// navigator.hardwareConcurrency + plugins — defeat navigator fingerprinting.
try {
if (typeof navigator !== 'undefined') {
if (navigator.hardwareConcurrency === undefined) {
Object.defineProperty(navigator, 'hardwareConcurrency', {value:8,configurable:true});
}
if (!navigator.plugins || navigator.plugins.length === 0) {
Object.defineProperty(navigator, 'plugins', {value:[{name:'Chrome PDF Plugin'},{name:'Chrome PDF Viewer'}],configurable:true});
}
if (!navigator.userAgent || !navigator.userAgent.includes('Chrome')) {
try { Object.defineProperty(navigator, 'userAgent', {value:'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36',configurable:true}); } catch(e2) {}
}
}
} catch(e) {}
// Timezone: non-UTC to defeat timezone-based sandbox detection.
try {
if (Date.prototype.getTimezoneOffset() === 0) {
var __origTZO = Date.prototype.getTimezoneOffset;
Date.prototype.getTimezoneOffset = function() { return -300; };
}
} catch(e) {}
// AudioBuffer — some evasions check this separately from AudioContext
if (typeof AudioBuffer === 'undefined') {
var AudioBuffer = function() { this.length = 0; this.sampleRate = 44100; };
}
// SpeechSynthesis
if (typeof speechSynthesis === 'undefined') {
var speechSynthesis = { speak: function(){}, cancel: function(){}, getVoices: function(){return [];} };
}
// self/globalThis — service workers use these
if (typeof self === 'undefined') var self = globalThis;
// ─── CommonJS / Webpack module polyfills ────────────────────────
// Webpack bundles wrap code in (function(module, exports, __webpack_require__){ })
// and call require(). Without these, bundled extensions crash on load.
if (typeof module === 'undefined') {
var module = { exports: {} };
}
if (typeof exports === 'undefined') {
var exports = module.exports;
}
if (typeof require === 'undefined') {
var __modules = {};
var require = function(id) {
if (__modules[id]) return __modules[id];
return {};
};
require.resolve = function(id) { return id; };
require.cache = __modules;
}
// window — many extensions assume DOM context even in service workers
if (typeof window === 'undefined') {
var window = globalThis;
}
// document stub — prevents crashes when extensions reference document.*
if (typeof document === 'undefined') {
var document = {
createElement: function(tag) {
var el = {
tagName: tag.toUpperCase(),
style: {},
children: [],
innerHTML: '',
textContent: '',
setAttribute: function(k, v) { this[k] = v; },
getAttribute: function(k) { return this[k] || null; },
appendChild: function(child) { this.children.push(child); return child; },
removeChild: function(child) { return child; },
addEventListener: function() {},
removeEventListener: function() {},
dispatchEvent: function() { return true; },
cloneNode: function() { return Object.assign({}, this); },
};
// Track innerHTML as a sink — XSS detection
Object.defineProperty(el, 'innerHTML', {
get: function() { return this._innerHTML || ''; },
set: function(v) {
this._innerHTML = v;
if (typeof v === 'string' && (v.indexOf('<script') >= 0 || v.indexOf('onerror') >= 0 || v.indexOf('javascript:') >= 0)) {
__jsdet_call('SINK:CWE-79:innerHTML', [v]);
}
if (typeof __jsdet_check_taint_at_sink === 'function') {
__jsdet_check_taint_at_sink('innerHTML', v);
}
}
});
return el;
},
getElementById: function() { return null; },
querySelector: function() { return null; },
querySelectorAll: function() { return []; },
body: null,
head: null,
documentElement: null,
readyState: 'complete',
addEventListener: function() {},
removeEventListener: function() {},
};
}
// navigator — used for browser/platform detection
if (typeof navigator === 'undefined') {
var navigator = {
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36',
language: 'en-US',
languages: ['en-US', 'en'],
platform: 'Linux x86_64',
onLine: true,
};
}
// location — used for URL checks
if (typeof location === 'undefined') {
var location = {
href: 'chrome-extension://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/background.html',
protocol: 'chrome-extension:',
hostname: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
pathname: '/background.html',
origin: 'chrome-extension://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
};
}
// Event/CustomEvent — needed for event dispatching
if (typeof Event === 'undefined') {
function Event(type, opts) { this.type = type; this.bubbles = (opts&&opts.bubbles)||false; }
}
if (typeof CustomEvent === 'undefined') {
function CustomEvent(type, opts) { this.type = type; this.detail = (opts&&opts.detail)||null; }
}
// ─── Bridge aliases ─────────────────────────────────────────────
// Bridge aliases: __jsdet_call and __jsdet_get route through __jsdet_bridge_call.
// The C glue registers __jsdet_bridge_call(api, args_json) → WASM host import.
// Our bootstrap uses __jsdet_call(api, args_array) for convenience.
function __jsdet_call(api, args) {
return __jsdet_bridge_call(api, JSON.stringify(args || []));
}
function __jsdet_get(object, property) {
return __jsdet_bridge_call(object + '.' + property, '[]');
}
// Provide atob/btoa (standard in browsers/service workers, missing in QuickJS).
var atob = (typeof atob !== 'undefined') ? atob : function(encoded) {
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
var result = '';
encoded = String(encoded).replace(/=+$/, '');
for (var i = 0; i < encoded.length; i += 4) {
var a = chars.indexOf(encoded.charAt(i));
var b = chars.indexOf(encoded.charAt(i + 1));
var c = chars.indexOf(encoded.charAt(i + 2));
var d = chars.indexOf(encoded.charAt(i + 3));
result += String.fromCharCode((a << 2) | (b >> 4));
if (c !== 64 && c >= 0) result += String.fromCharCode(((b & 15) << 4) | (c >> 2));
if (d !== 64 && d >= 0) result += String.fromCharCode(((c & 3) << 6) | d);
}
return result;
};
if (typeof globalThis !== 'undefined') globalThis.atob = atob;
var btoa = (typeof btoa !== 'undefined') ? btoa : function(str) {
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
var result = '';
for (var i = 0; i < str.length; i += 3) {
var a = str.charCodeAt(i);
var b = i + 1 < str.length ? str.charCodeAt(i + 1) : 0;
var c = i + 2 < str.length ? str.charCodeAt(i + 2) : 0;
result += chars[a >> 2] + chars[((a & 3) << 4) | (b >> 4)];
result += (i + 1 < str.length) ? chars[((b & 15) << 2) | (c >> 6)] : '=';
result += (i + 2 < str.length) ? chars[c & 63] : '=';
}
return result;
};
if (typeof globalThis !== 'undefined') globalThis.btoa = btoa;
// Override dangerous globals to route through bridge for observation.
// This is THE critical hook — every code-injection path ends with eval/Function.
// Each override: (1) reports via bridge, (2) checks engine-level taint.
var __orig_eval = eval;
eval = function(code) {
var codeStr = String(code);
__jsdet_call('eval', [codeStr]);
// Engine-level taint check: if the code string carries a taint label,
// report taint-confirmed code injection.
if (typeof __jsdet_check_taint_at_sink === 'function') {
__jsdet_check_taint_at_sink('eval', codeStr);
}
return undefined;
};
// Also override on window/self/globalThis to prevent bracket-notation evasion
// (window["eval"](code) would bypass global eval override without this)
// Force-override eval on all global objects via defineProperty.
// Simple assignment (self.eval = eval) doesn't override the built-in
// eval in QuickJS because eval is a special property. defineProperty
// with configurable:true forces it.
try { Object.defineProperty(globalThis, 'eval', {value:eval,writable:true,configurable:true}); } catch(e) {}
try { if (typeof self !== 'undefined') Object.defineProperty(self, 'eval', {value:eval,writable:true,configurable:true}); } catch(e) {}
try { eval.toString = function(){return 'function eval() { [native code] }';}; } catch(e) {}
var __orig_Function = Function;
Function = function() {
var body = arguments.length > 0 ? String(arguments[arguments.length - 1]) : '';
__jsdet_call('Function', [body]);
if (typeof __jsdet_check_taint_at_sink === 'function') {
__jsdet_check_taint_at_sink('Function', body);
}
return function() {};
};
if (typeof self !== 'undefined') self.Function = Function;
if (typeof globalThis !== 'undefined') globalThis.Function = Function;
try { Function.toString = function(){return 'function Function() { [native code] }';}; } catch(e) {}
// Prevent constructor chain evasion: [].constructor.constructor(code)()
// Override Function on common prototype chains
try { Object.getPrototypeOf([]).constructor.constructor = Function; } catch(e) {}
try { Object.getPrototypeOf({}).constructor.constructor = Function; } catch(e) {}
try { Object.getPrototypeOf('').constructor.constructor = Function; } catch(e) {}
try { (0).constructor.constructor = Function; } catch(e) {}
// Override RegExp for ReDoS detection — user-controlled regex is a DoS vector.
var __orig_RegExp = RegExp;
RegExp = function() {
var pattern = arguments.length > 0 ? String(arguments[0]) : '';
__jsdet_call('RegExp', [pattern]);
if (typeof __jsdet_check_taint_at_sink === 'function') {
__jsdet_check_taint_at_sink('RegExp', pattern);
}
return new __orig_RegExp(pattern, arguments[1]);
};
RegExp.prototype = __orig_RegExp.prototype;
try { RegExp.toString = function(){return 'function RegExp() { [native code] }';}; } catch(e) {}
// Override fetch for SSRF detection + taint check.
var __orig_fetch = (typeof fetch === 'function') ? fetch : null;
function fetch(url, opts) {
var urlStr = typeof url === 'string' ? url : (url && url.url) || String(url);
__jsdet_call('fetch', [urlStr]);
if (typeof __jsdet_check_taint_at_sink === 'function') {
__jsdet_check_taint_at_sink('fetch', urlStr);
}
// Return a resolved promise (no real network in sandbox).
return {then: function(cb) { if(cb) cb({ok:true,status:200,json:function(){return {};},text:function(){return '';}}); return this; }};
}
try { fetch.toString = function(){return 'function fetch() { [native code] }';}; } catch(e) {}
"#;
const CHROME_RUNTIME: &str = r#"
chrome.runtime = chrome.runtime || {};
chrome.runtime.id = __jsdet_get('chrome.runtime', 'id') || 'abcdefghijklmnopabcdefghijklmnop';
chrome.runtime.lastError = null;
chrome.runtime.getURL = function(path) {
return __jsdet_call('chrome.runtime.getURL', [path]);
};
chrome.runtime.sendMessage = function(msg, callback) {
var result = __jsdet_call('chrome.runtime.sendMessage', [JSON.stringify(msg)]);
if (callback) callback(result);
};
chrome.runtime.onMessage = {
_listeners: [],
addListener: function(fn) { this._listeners.push(fn); },
removeListener: function(fn) {
this._listeners = this._listeners.filter(function(l) { return l !== fn; });
}
};
chrome.runtime.onMessageExternal = {
_listeners: [],
addListener: function(fn) { this._listeners.push(fn); },
removeListener: function(fn) {
this._listeners = this._listeners.filter(function(l) { return l !== fn; });
}
};
chrome.runtime.onInstalled = {
addListener: function(fn) { fn({reason: 'install'}); }
};
chrome.runtime.getManifest = function() {
return JSON.parse(__jsdet_call('chrome.runtime.getManifest', []));
};
chrome.runtime.connect = function(info) {
return {
postMessage: function(msg) {
__jsdet_call('chrome.runtime.sendMessage', [JSON.stringify(msg)]);
},
onMessage: { addListener: function(fn) {} },
onDisconnect: { addListener: function(fn) {} }
};
};
// Host-callable: add additional property names to the schema probe.
// Called before _discoverSchema with identifiers extracted from the extension source.
chrome.runtime._extraSchemaProps = [];
chrome.runtime._addSchemaProps = function(props) {
for (var i = 0; i < props.length; i++) {
if (chrome.runtime._extraSchemaProps.indexOf(props[i]) === -1) {
chrome.runtime._extraSchemaProps.push(props[i]);
}
}
};
// Host-callable: discover message schema by observing handler property access.
// Sends a message with ALL string values tainted by unique labels.
// When tainted values reach sinks, the label reveals which property was used.
// Returns: array of property names the handler accessed.
chrome.runtime._discoverSchema = function(sender) {
sender = sender || {tab: {id: 1, url: 'https://example.com'}, id: chrome.runtime.id};
var accessed = [];
// Create a message with getters that log access.
// We use defineProperty to intercept reads on common names.
var probeMsg = {};
var propNames = [
// Dispatch properties
'action', 'type', 'command', 'cmd', 'method', 'event', 'op',
'kind', 'name', 'task', 'msg', 'request', 'intent', 'route',
// Data properties
'code', 'url', 'html', 'data', 'text', 'value', 'payload',
'script', 'query', 'input', 'target', 'endpoint', 'content',
'body', 'message', 'params', 'args', 'options', 'config',
'key', 'id', 'token', 'path', 'file', 'domain', 'host',
'src', 'href', 'redirect', 'callback', 'handler', 'func',
// Extension-specific (common patterns)
'tabId', 'windowId', 'frameId', 'senderId', 'extensionId',
'u2p', 'profile', 'settings', 'preferences', 'state',
'enabled', 'active', 'visible', 'mode', 'source', 'origin',
];
// Add dynamically injected property names from source analysis
for (var k = 0; k < chrome.runtime._extraSchemaProps.length; k++) {
if (propNames.indexOf(chrome.runtime._extraSchemaProps[k]) === -1) {
propNames.push(chrome.runtime._extraSchemaProps[k]);
}
}
for (var i = 0; i < propNames.length; i++) {
(function(prop) {
Object.defineProperty(probeMsg, prop, {
get: function() {
accessed.push(prop);
// Return a tainted probe value
var val = 'SLN_PROBE_' + prop;
if (typeof __jsdet_set_taint === 'function') {
__jsdet_set_taint(val, 1);
}
return val;
},
enumerable: true,
configurable: true,
});
})(propNames[i]);
}
// Also intercept unknown property access via __proto__ getter
// (limited but catches some patterns)
// Fire all listeners with the probe message
for (var j = 0; j < chrome.runtime.onMessage._listeners.length; j++) {
try {
chrome.runtime.onMessage._listeners[j](probeMsg, sender, function() {});
} catch(e) {}
}
// Report discovered schema via bridge
if (accessed.length > 0) {
__jsdet_call('__schema_discovered', [JSON.stringify(accessed)]);
}
return accessed;
};
// Host-callable: fire onMessage listeners with crafted data.
chrome.runtime._fireOnMessage = function(msg, sender, sendResponse) {
sender = sender || {tab: {id: 1, url: 'https://example.com'}, id: chrome.runtime.id};
sendResponse = sendResponse || function() {};
// Taint all string values in the message — they represent attacker input.
// This enables taint tracking to confirm that input reaches sinks.
if (typeof __jsdet_set_taint === 'function' && msg && typeof msg === 'object') {
for (var k in msg) {
if (typeof msg[k] === 'string') __jsdet_set_taint(msg[k], 1);
}
}
for (var i = 0; i < chrome.runtime.onMessage._listeners.length; i++) {
try { chrome.runtime.onMessage._listeners[i](msg, sender, sendResponse); } catch(e) {}
}
};
chrome.runtime._fireOnMessageExternal = function(msg, sender, sendResponse) {
sender = sender || {id: 'external-extension', url: 'https://external.com'};
sendResponse = sendResponse || function() {};
if (typeof __jsdet_set_taint === 'function' && msg && typeof msg === 'object') {
for (var k in msg) {
if (typeof msg[k] === 'string') __jsdet_set_taint(msg[k], 1);
}
}
for (var i = 0; i < chrome.runtime.onMessageExternal._listeners.length; i++) {
try { chrome.runtime.onMessageExternal._listeners[i](msg, sender, sendResponse); } catch(e) {}
}
};
"#;
const CHROME_TABS: &str = r#"
chrome.tabs = chrome.tabs || {};
chrome.tabs.query = function(info, callback) {
__jsdet_call('chrome.tabs.query', [JSON.stringify(info || {})]);
// Return a realistic tab array so extensions that access tabs[0].url work
var result = [{id: 1, url: 'https://example.com/sensitive-page', title: 'Example', active: true, windowId: 1, index: 0, pinned: false, incognito: false}];
if (callback) callback(result);
return result;
};
chrome.tabs.create = function(props, callback) {
var result = JSON.parse(__jsdet_call('chrome.tabs.create', [JSON.stringify(props)]));
if (callback) callback(result);
};
chrome.tabs.update = function(tabId, props, callback) {
__jsdet_call('chrome.tabs.update', [String(tabId), JSON.stringify(props)]);
if (callback) callback();
};
chrome.tabs.executeScript = function(tabId, details, callback) {
__jsdet_call('chrome.tabs.executeScript', [String(tabId), JSON.stringify(details)]);
if (callback) callback();
};
chrome.tabs.sendMessage = function(tabId, msg, callback) {
__jsdet_call('chrome.tabs.sendMessage', [String(tabId), JSON.stringify(msg)]);
if (callback) callback();
};
chrome.tabs.onUpdated = {
_listeners: [],
addListener: function(fn) { this._listeners.push(fn); }
};
chrome.tabs._fireOnUpdated = function(tabId, changeInfo, tab) {
for (var i = 0; i < chrome.tabs.onUpdated._listeners.length; i++) {
try { chrome.tabs.onUpdated._listeners[i](tabId, changeInfo, tab); } catch(e) {}
}
};
chrome.tabs.onRemoved = {
_listeners: [],
addListener: function(fn) { this._listeners.push(fn); }
};
"#;
const CHROME_COOKIES: &str = r#"
chrome.cookies = chrome.cookies || {};
chrome.cookies.getAll = function(details, callback) {
var result = JSON.parse(__jsdet_call('chrome.cookies.getAll', [JSON.stringify(details || {})]));
if (callback) callback(result);
return result;
};
chrome.cookies.get = function(details, callback) {
var result = JSON.parse(__jsdet_call('chrome.cookies.get', [JSON.stringify(details)]));
if (callback) callback(result);
};
chrome.cookies.set = function(details, callback) {
__jsdet_call('chrome.cookies.set', [JSON.stringify(details)]);
if (callback) callback();
};
chrome.cookies.remove = function(details, callback) {
__jsdet_call('chrome.cookies.remove', [JSON.stringify(details)]);
if (callback) callback();
};
"#;
const CHROME_STORAGE: &str = r#"
chrome.storage = chrome.storage || {};
// In-memory storage backing — allows set/get roundtrips within a session.
// This is critical for detecting storage-mediated attacks where payloads
// are stored then retrieved and eval'd.
var __storage_local = {};
var __storage_sync = {};
function __storage_make(store, name) {
return {
get: function(keys, callback) {
__jsdet_call('chrome.storage.' + name + '.get', [JSON.stringify(keys || null)]);
var result = {};
if (keys === null || keys === undefined) {
for (var k in store) result[k] = store[k];
} else {
var arr = typeof keys === 'string' ? [keys] : keys;
for (var i = 0; i < arr.length; i++) {
if (arr[i] in store) result[arr[i]] = store[arr[i]];
}
}
if (callback) callback(result);
},
set: function(items, callback) {
__jsdet_call('chrome.storage.' + name + '.set', [JSON.stringify(items)]);
for (var k in items) store[k] = items[k];
if (callback) callback();
},
remove: function(keys, callback) {
__jsdet_call('chrome.storage.' + name + '.remove', [JSON.stringify(keys)]);
var arr = typeof keys === 'string' ? [keys] : keys;
for (var i = 0; i < arr.length; i++) delete store[arr[i]];
if (callback) callback();
},
clear: function(callback) {
__jsdet_call('chrome.storage.' + name + '.clear', []);
for (var k in store) delete store[k];
if (callback) callback();
}
};
}
chrome.storage.local = __storage_make(__storage_local, 'local');
chrome.storage.sync = __storage_make(__storage_sync, 'sync');
chrome.storage.onChanged = {
addListener: function(fn) {}
};
"#;
const CHROME_WEB_REQUEST: &str = r#"
chrome.webRequest = chrome.webRequest || {};
chrome.webRequest.onBeforeRequest = {
addListener: function(fn, filter, extraInfo) {
__jsdet_call('chrome.webRequest.onBeforeRequest.addListener', []);
}
};
chrome.webRequest.onBeforeSendHeaders = {
_listeners: [],
addListener: function(fn, filter, extraInfo) {
this._listeners.push(fn);
__jsdet_call('chrome.webRequest.onBeforeSendHeaders.addListener', []);
}
};
chrome.webRequest._fireOnBeforeSendHeaders = function(details) {
details = details || {url:'https://example.com/api',method:'GET',requestHeaders:[{name:'Cookie',value:'session=abc123'}]};
for (var i = 0; i < chrome.webRequest.onBeforeSendHeaders._listeners.length; i++) {
try { var result = chrome.webRequest.onBeforeSendHeaders._listeners[i](details);
if (result && result.requestHeaders) __jsdet_call('chrome.webRequest.modifyHeaders', [JSON.stringify(result.requestHeaders)]);
} catch(e) {}
}
};
chrome.webRequest.onCompleted = {
addListener: function(fn, filter) {}
};
chrome.webRequest.onErrorOccurred = {
addListener: function(fn, filter) {}
};
"#;
const CHROME_ALARMS: &str = r#"
chrome.alarms = chrome.alarms || {};
chrome.alarms.create = function(name, info) {
__jsdet_call('chrome.alarms.create', [name, JSON.stringify(info)]);
};
chrome.alarms.get = function(name, callback) {
var result = JSON.parse(__jsdet_call('chrome.alarms.get', [name]));
if (callback) callback(result);
};
chrome.alarms.getAll = function(callback) {
var result = JSON.parse(__jsdet_call('chrome.alarms.getAll', []));
if (callback) callback(result);
};
chrome.alarms.clear = function(name, callback) {
__jsdet_call('chrome.alarms.clear', [name]);
if (callback) callback(true);
};
chrome.alarms.onAlarm = {
_listeners: [],
addListener: function(fn) { this._listeners.push(fn); }
};
chrome.alarms._fireOnAlarm = function(alarm) {
alarm = alarm || {name: 'soleno_probe', scheduledTime: Date.now()};
for (var i = 0; i < chrome.alarms.onAlarm._listeners.length; i++) {
try { chrome.alarms.onAlarm._listeners[i](alarm); } catch(e) {}
}
};
"#;
const CHROME_SCRIPTING: &str = r#"
chrome.scripting = chrome.scripting || {};
chrome.scripting.executeScript = function(injection, callback) {
__jsdet_call('chrome.scripting.executeScript', [JSON.stringify(injection)]);
if (callback) callback([]);
};
chrome.scripting.insertCSS = function(injection, callback) {
__jsdet_call('chrome.scripting.insertCSS', [JSON.stringify(injection)]);
if (callback) callback();
};
chrome.scripting.registerContentScripts = function(scripts, callback) {
__jsdet_call('chrome.scripting.registerContentScripts', [JSON.stringify(scripts)]);
if (callback) callback();
};
"#;
const CHROME_PERMISSIONS: &str = r#"
chrome.permissions = chrome.permissions || {};
chrome.permissions.getAll = function(callback) {
var result = JSON.parse(__jsdet_call('chrome.permissions.getAll', []));
if (callback) callback(result);
};
chrome.permissions.contains = function(perms, callback) {
if (callback) callback(true);
};
chrome.permissions.request = function(perms, callback) {
if (callback) callback(true);
};
"#;
const CHROME_IDENTITY: &str = r#"
chrome.identity = chrome.identity || {};
chrome.identity.getAuthToken = function(details, callback) {
__jsdet_call('chrome.identity.getAuthToken', [JSON.stringify(details || {})]);
if (callback) callback('fake_oauth_token_SLN_IDENTITY');
};
chrome.identity.getProfileUserInfo = function(details, callback) {
__jsdet_call('chrome.identity.getProfileUserInfo', [JSON.stringify(details || {})]);
if (callback) callback({email: 'user@example.com', id: '12345'});
};
chrome.identity.launchWebAuthFlow = function(details, callback) {
__jsdet_call('chrome.identity.launchWebAuthFlow', [JSON.stringify(details)]);
if (callback) callback(details.url || 'https://auth.example.com/callback');
};
"#;
const CHROME_BOOKMARKS: &str = r#"
chrome.bookmarks = chrome.bookmarks || {};
chrome.bookmarks.getTree = function(callback) {
__jsdet_call('chrome.bookmarks.getTree', []);
if (callback) callback([{id:'0',title:'',children:[{id:'1',title:'Bookmarks Bar',url:'',children:[]}]}]);
};
chrome.bookmarks.search = function(query, callback) {
__jsdet_call('chrome.bookmarks.search', [JSON.stringify(query)]);
if (callback) callback([]);
};
chrome.bookmarks.create = function(bookmark, callback) {
__jsdet_call('chrome.bookmarks.create', [JSON.stringify(bookmark)]);
if (callback) callback({id:'999',title:bookmark.title||'',url:bookmark.url||''});
};
chrome.bookmarks.remove = function(id, callback) {
__jsdet_call('chrome.bookmarks.remove', [id]);
if (callback) callback();
};
chrome.bookmarks.update = function(id, changes, callback) {
__jsdet_call('chrome.bookmarks.update', [id, JSON.stringify(changes)]);
if (callback) callback({id:id,title:changes.title||'',url:changes.url||''});
};
chrome.bookmarks.onCreated = {
_listeners: [],
addListener: function(fn) { this._listeners.push(fn); }
};
chrome.bookmarks._fireOnCreated = function(id, bookmark) {
id = id || '1';
bookmark = bookmark || {id:'1',title:'Test',url:'https://amazon.com/product?tag=legit-20'};
for (var i = 0; i < chrome.bookmarks.onCreated._listeners.length; i++) {
try { chrome.bookmarks.onCreated._listeners[i](id, bookmark); } catch(e) {}
}
};
"#;
const CHROME_HISTORY: &str = r#"
chrome.history = chrome.history || {};
chrome.history.search = function(query, callback) {
__jsdet_call('chrome.history.search', [JSON.stringify(query)]);
if (callback) callback([{id:'1',url:'https://example.com',title:'Example',lastVisitTime:Date.now()}]);
};
chrome.history.deleteUrl = function(details, callback) {
__jsdet_call('chrome.history.deleteUrl', [JSON.stringify(details)]);
if (callback) callback();
};
chrome.history.deleteAll = function(callback) {
__jsdet_call('chrome.history.deleteAll', []);
if (callback) callback();
};
"#;
const CHROME_DOWNLOADS: &str = r#"
chrome.downloads = chrome.downloads || {};
chrome.downloads.download = function(options, callback) {
__jsdet_call('chrome.downloads.download', [JSON.stringify(options)]);
if (callback) callback(999);
};
chrome.downloads.search = function(query, callback) {
__jsdet_call('chrome.downloads.search', [JSON.stringify(query)]);
if (callback) callback([]);
};
chrome.downloads.onChanged = { addListener: function(fn) {} };
"#;
const CHROME_NOTIFICATIONS: &str = r#"
chrome.notifications = chrome.notifications || {};
chrome.notifications.create = function(id, options, callback) {
__jsdet_call('chrome.notifications.create', [id || '', JSON.stringify(options || {})]);
if (callback) callback(id || 'notif_1');
};
chrome.notifications.clear = function(id, callback) {
__jsdet_call('chrome.notifications.clear', [id]);
if (callback) callback(true);
};
chrome.notifications.onClicked = { addListener: function(fn) {} };
chrome.notifications.onClosed = { addListener: function(fn) {} };
"#;
const CHROME_ACTION: &str = r#"
chrome.action = chrome.action || {};
chrome.action.setIcon = function(details, callback) { if (callback) callback(); };
chrome.action.setBadgeText = function(details, callback) { if (callback) callback(); };
chrome.action.setBadgeBackgroundColor = function(details, callback) { if (callback) callback(); };
chrome.action.setTitle = function(details, callback) { if (callback) callback(); };
chrome.action.setPopup = function(details, callback) { if (callback) callback(); };
chrome.action.onClicked = { addListener: function(fn) {} };
// Alias browserAction for MV2 compat
chrome.browserAction = chrome.action;
"#;
const CHROME_WINDOWS: &str = r#"
chrome.windows = chrome.windows || {};
chrome.windows.getCurrent = function(getInfo, callback) {
if (typeof getInfo === 'function') { callback = getInfo; getInfo = {}; }
if (callback) callback({id: 1, focused: true, type: 'normal', state: 'maximized'});
};
chrome.windows.getAll = function(getInfo, callback) {
if (typeof getInfo === 'function') { callback = getInfo; getInfo = {}; }
if (callback) callback([{id: 1, focused: true, type: 'normal'}]);
};
chrome.windows.create = function(createData, callback) {
__jsdet_call('chrome.windows.create', [JSON.stringify(createData || {})]);
if (callback) callback({id: 2, focused: true});
};
chrome.windows.update = function(windowId, updateInfo, callback) {
__jsdet_call('chrome.windows.update', [String(windowId), JSON.stringify(updateInfo)]);
if (callback) callback({id: windowId});
};
chrome.windows.WINDOW_ID_CURRENT = -2;
chrome.windows.WINDOW_ID_NONE = -1;
"#;
const CHROME_CONTEXT_MENUS: &str = r#"
chrome.contextMenus = chrome.contextMenus || {};
chrome.contextMenus.create = function(createProperties, callback) {
__jsdet_call('chrome.contextMenus.create', [JSON.stringify(createProperties)]);
if (callback) callback();
return createProperties.id || 'menu_1';
};
chrome.contextMenus.update = function(id, updateProperties, callback) {
if (callback) callback();
};
chrome.contextMenus.remove = function(id, callback) {
if (callback) callback();
};
chrome.contextMenus.removeAll = function(callback) {
if (callback) callback();
};
chrome.contextMenus.onClicked = { addListener: function(fn) {} };
"#;
const CHROME_I18N: &str = r#"
chrome.i18n = chrome.i18n || {};
chrome.i18n.getMessage = function(messageName, substitutions) {
return messageName || '';
};
chrome.i18n.getUILanguage = function() {
return 'en';
};
chrome.i18n.detectLanguage = function(text, callback) {
if (callback) callback({isReliable: true, languages: [{language: 'en', percentage: 100}]});
};
"#;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bootstrap_includes_runtime() {
let manifest =
Manifest::parse(r#"{"name":"T","manifest_version":3,"version":"1.0"}"#).unwrap();
let js = generate_bootstrap(&manifest);
assert!(js.contains("chrome.runtime"));
assert!(js.contains("_fireOnMessage"));
}
#[test]
fn bootstrap_includes_tabs_when_permitted() {
let manifest = Manifest::parse(
r#"{
"name":"T","manifest_version":3,"version":"1.0",
"permissions":["tabs"]
}"#,
)
.unwrap();
let js = generate_bootstrap(&manifest);
assert!(js.contains("chrome.tabs.query"));
assert!(js.contains("chrome.tabs.executeScript"));
}
#[test]
fn bootstrap_excludes_tabs_without_permission() {
let manifest = Manifest::parse(
r#"{
"name":"T","manifest_version":3,"version":"1.0",
"permissions":["storage"]
}"#,
)
.unwrap();
let js = generate_bootstrap(&manifest);
assert!(!js.contains("chrome.tabs.query"));
assert!(js.contains("chrome.storage")); }
#[test]
fn bootstrap_permission_gated() {
let manifest = Manifest::parse(
r#"{
"name":"T","manifest_version":3,"version":"1.0",
"permissions":["tabs","cookies","storage","alarms","webRequest"]
}"#,
)
.unwrap();
let js = generate_bootstrap(&manifest);
assert!(js.contains("chrome.tabs"));
assert!(js.contains("chrome.cookies"));
assert!(js.contains("chrome.storage"));
assert!(js.contains("chrome.alarms"));
assert!(js.contains("chrome.webRequest"));
}
#[test]
fn permission_tabs_enables_tabs_api() {
let manifest = Manifest::parse(
r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0",
"permissions": ["tabs"]
}"#,
)
.unwrap();
let js = generate_bootstrap(&manifest);
assert!(js.contains("chrome.tabs"));
assert!(js.contains("chrome.tabs.query"));
assert!(js.contains("chrome.tabs.create"));
assert!(js.contains("chrome.tabs.update"));
assert!(js.contains("chrome.tabs.executeScript"));
assert!(js.contains("chrome.tabs.sendMessage"));
}
#[test]
fn permission_cookies_enables_cookies_api() {
let manifest = Manifest::parse(
r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0",
"permissions": ["cookies"]
}"#,
)
.unwrap();
let js = generate_bootstrap(&manifest);
assert!(js.contains("chrome.cookies"));
assert!(js.contains("chrome.cookies.getAll"));
assert!(js.contains("chrome.cookies.get"));
assert!(js.contains("chrome.cookies.set"));
assert!(js.contains("chrome.cookies.remove"));
}
#[test]
fn permission_storage_enables_storage_api() {
let manifest = Manifest::parse(
r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0",
"permissions": ["storage"]
}"#,
)
.unwrap();
let js = generate_bootstrap(&manifest);
assert!(js.contains("chrome.storage"));
assert!(js.contains("chrome.storage.local"));
assert!(js.contains("chrome.storage.sync"));
}
#[test]
fn permission_webrequest_enables_webrequest_api() {
let manifest = Manifest::parse(
r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0",
"permissions": ["webRequest"]
}"#,
)
.unwrap();
let js = generate_bootstrap(&manifest);
assert!(js.contains("chrome.webRequest"));
assert!(js.contains("chrome.webRequest.onBeforeRequest"));
}
#[test]
fn permission_webrequest_blocking_enables_webrequest_api() {
let manifest = Manifest::parse(
r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0",
"permissions": ["webRequestBlocking"]
}"#,
)
.unwrap();
let js = generate_bootstrap(&manifest);
assert!(js.contains("chrome.webRequest"));
}
#[test]
fn permission_alarms_enables_alarms_api() {
let manifest = Manifest::parse(
r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0",
"permissions": ["alarms"]
}"#,
)
.unwrap();
let js = generate_bootstrap(&manifest);
assert!(js.contains("chrome.alarms"));
assert!(js.contains("chrome.alarms.create"));
assert!(js.contains("chrome.alarms.get"));
}
#[test]
fn permission_host_wildcard_enables_tabs_api() {
let manifest = Manifest::parse(
r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0",
"host_permissions": ["*://*/*"]
}"#,
)
.unwrap();
let js = generate_bootstrap(&manifest);
assert!(js.contains("chrome.tabs"));
}
#[test]
fn permission_host_all_urls_no_tabs() {
let manifest = Manifest::parse(
r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0",
"host_permissions": ["<all_urls>"]
}"#,
)
.unwrap();
let js = generate_bootstrap(&manifest);
assert!(!js.contains("chrome.tabs"));
}
#[test]
fn no_permissions_has_runtime() {
let manifest = Manifest::parse(
r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0"
}"#,
)
.unwrap();
let js = generate_bootstrap(&manifest);
assert!(js.contains("chrome.runtime"));
}
#[test]
fn no_permissions_has_scripting() {
let manifest = Manifest::parse(
r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0"
}"#,
)
.unwrap();
let js = generate_bootstrap(&manifest);
assert!(js.contains("chrome.scripting"));
assert!(js.contains("chrome.scripting.executeScript"));
}
#[test]
fn no_permissions_has_permissions() {
let manifest = Manifest::parse(
r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0"
}"#,
)
.unwrap();
let js = generate_bootstrap(&manifest);
assert!(js.contains("chrome.permissions"));
}
#[test]
fn no_permissions_no_tabs() {
let manifest = Manifest::parse(
r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0"
}"#,
)
.unwrap();
let js = generate_bootstrap(&manifest);
assert!(!js.contains("chrome.tabs"));
}
#[test]
fn no_permissions_no_cookies() {
let manifest = Manifest::parse(
r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0"
}"#,
)
.unwrap();
let js = generate_bootstrap(&manifest);
assert!(!js.contains("chrome.cookies"));
}
#[test]
fn no_permissions_no_storage() {
let manifest = Manifest::parse(
r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0"
}"#,
)
.unwrap();
let js = generate_bootstrap(&manifest);
assert!(!js.contains("chrome.storage"));
}
#[test]
fn no_permissions_no_webrequest() {
let manifest = Manifest::parse(
r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0"
}"#,
)
.unwrap();
let js = generate_bootstrap(&manifest);
assert!(!js.contains("chrome.webRequest"));
}
#[test]
fn no_permissions_no_alarms() {
let manifest = Manifest::parse(
r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0"
}"#,
)
.unwrap();
let js = generate_bootstrap(&manifest);
assert!(!js.contains("chrome.alarms"));
}
#[test]
fn all_permissions_enables_everything() {
let manifest = Manifest::parse(r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0",
"permissions": ["tabs", "cookies", "storage", "webRequest", "webRequestBlocking", "alarms", "history", "bookmarks"]
}"#).unwrap();
let js = generate_bootstrap(&manifest);
assert!(js.contains("chrome.runtime"));
assert!(js.contains("chrome.scripting"));
assert!(js.contains("chrome.permissions"));
assert!(js.contains("chrome.tabs"));
assert!(js.contains("chrome.cookies"));
assert!(js.contains("chrome.storage"));
assert!(js.contains("chrome.webRequest"));
assert!(js.contains("chrome.alarms"));
}
#[test]
fn bootstrap_js_is_non_empty() {
let manifest = Manifest::default();
let js = generate_bootstrap(&manifest);
assert!(!js.is_empty());
assert!(js.len() > 500); }
#[test]
fn bootstrap_js_has_balanced_braces() {
let manifest = Manifest::default();
let js = generate_bootstrap(&manifest);
let open_count = js.matches('{').count();
let close_count = js.matches('}').count();
assert_eq!(open_count, close_count, "Unbalanced braces in bootstrap JS");
let open_paren = js.matches('(').count();
let close_paren = js.matches(')').count();
assert_eq!(
open_paren, close_paren,
"Unbalanced parentheses in bootstrap JS"
);
let open_bracket = js.matches('[').count();
let close_bracket = js.matches(']').count();
assert_eq!(
open_bracket, close_bracket,
"Unbalanced brackets in bootstrap JS"
);
}
#[test]
fn bootstrap_js_no_syntax_errors_basic() {
let manifest = Manifest::default();
let js = generate_bootstrap(&manifest);
assert!(!js.contains(";;")); assert!(!js.contains(".."));
assert!(js.contains("function("));
}
#[test]
fn bootstrap_js_contains_jsdet_call() {
let manifest = Manifest::default();
let js = generate_bootstrap(&manifest);
assert!(js.contains("__jsdet_call"));
}
#[test]
fn bootstrap_js_contains_jsdet_get() {
let manifest = Manifest::default();
let js = generate_bootstrap(&manifest);
assert!(js.contains("__jsdet_get"));
}
#[test]
fn runtime_uses_jsdet_call() {
let manifest = Manifest::default();
let js = generate_bootstrap(&manifest);
assert!(js.contains("__jsdet_call('chrome.runtime.getURL'"));
}
#[test]
fn runtime_get_manifest_uses_jsdet_call() {
let manifest = Manifest::default();
let js = generate_bootstrap(&manifest);
assert!(js.contains("__jsdet_call('chrome.runtime.getManifest'"));
}
#[test]
fn runtime_id_uses_jsdet_get() {
let manifest = Manifest::default();
let js = generate_bootstrap(&manifest);
assert!(js.contains("__jsdet_get('chrome.runtime', 'id')"));
}
#[test]
fn bootstrap_contains_fire_on_message() {
let manifest = Manifest::default();
let js = generate_bootstrap(&manifest);
assert!(js.contains("_fireOnMessage"));
}
#[test]
fn bootstrap_contains_fire_on_message_external() {
let manifest = Manifest::default();
let js = generate_bootstrap(&manifest);
assert!(js.contains("_fireOnMessageExternal"));
}
#[test]
fn bootstrap_contains_on_message_listeners() {
let manifest = Manifest::default();
let js = generate_bootstrap(&manifest);
assert!(js.contains("onMessage"));
assert!(js.contains("onMessageExternal"));
assert!(js.contains("_listeners"));
assert!(js.contains("addListener"));
}
#[test]
fn bootstrap_contains_runtime_connect() {
let manifest = Manifest::default();
let js = generate_bootstrap(&manifest);
assert!(js.contains("chrome.runtime.connect"));
}
#[test]
fn bootstrap_contains_runtime_last_error() {
let manifest = Manifest::default();
let js = generate_bootstrap(&manifest);
assert!(js.contains("chrome.runtime.lastError"));
}
#[test]
fn bootstrap_contains_runtime_on_installed() {
let manifest = Manifest::default();
let js = generate_bootstrap(&manifest);
assert!(js.contains("chrome.runtime.onInstalled"));
}
#[test]
fn storage_local_has_all_methods() {
let manifest = Manifest::parse(
r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0",
"permissions": ["storage"]
}"#,
)
.unwrap();
let js = generate_bootstrap(&manifest);
assert!(js.contains("chrome.storage.local"));
assert!(js.contains("__storage_make"));
assert!(js.contains("'local'"));
}
#[test]
fn storage_sync_has_all_methods() {
let manifest = Manifest::parse(
r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0",
"permissions": ["storage"]
}"#,
)
.unwrap();
let js = generate_bootstrap(&manifest);
assert!(js.contains("chrome.storage.sync"));
assert!(js.contains("__storage_make"));
assert!(js.contains("'sync'"));
}
#[test]
fn storage_has_on_changed() {
let manifest = Manifest::parse(
r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0",
"permissions": ["storage"]
}"#,
)
.unwrap();
let js = generate_bootstrap(&manifest);
assert!(js.contains("chrome.storage.onChanged"));
}
#[test]
fn tabs_has_all_methods() {
let manifest = Manifest::parse(
r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0",
"permissions": ["tabs"]
}"#,
)
.unwrap();
let js = generate_bootstrap(&manifest);
assert!(js.contains("chrome.tabs.query"));
assert!(js.contains("chrome.tabs.create"));
assert!(js.contains("chrome.tabs.update"));
assert!(js.contains("chrome.tabs.executeScript"));
assert!(js.contains("chrome.tabs.sendMessage"));
}
#[test]
fn tabs_has_event_listeners() {
let manifest = Manifest::parse(
r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0",
"permissions": ["tabs"]
}"#,
)
.unwrap();
let js = generate_bootstrap(&manifest);
assert!(js.contains("chrome.tabs.onUpdated"));
assert!(js.contains("chrome.tabs.onRemoved"));
}
#[test]
fn scripting_has_all_methods() {
let manifest = Manifest::default();
let js = generate_bootstrap(&manifest);
assert!(js.contains("chrome.scripting.executeScript"));
assert!(js.contains("chrome.scripting.insertCSS"));
assert!(js.contains("chrome.scripting.registerContentScripts"));
}
#[test]
fn alarms_has_all_methods() {
let manifest = Manifest::parse(
r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0",
"permissions": ["alarms"]
}"#,
)
.unwrap();
let js = generate_bootstrap(&manifest);
assert!(js.contains("chrome.alarms.create"));
assert!(js.contains("chrome.alarms.get"));
assert!(js.contains("chrome.alarms.getAll"));
assert!(js.contains("chrome.alarms.clear"));
}
#[test]
fn alarms_has_on_alarm() {
let manifest = Manifest::parse(
r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0",
"permissions": ["alarms"]
}"#,
)
.unwrap();
let js = generate_bootstrap(&manifest);
assert!(js.contains("chrome.alarms.onAlarm"));
}
#[test]
fn permissions_has_all_methods() {
let manifest = Manifest::default();
let js = generate_bootstrap(&manifest);
assert!(js.contains("chrome.permissions.getAll"));
assert!(js.contains("chrome.permissions.contains"));
assert!(js.contains("chrome.permissions.request"));
}
#[test]
fn webrequest_has_event_handlers() {
let manifest = Manifest::parse(
r#"{
"name": "Test",
"manifest_version": 3,
"version": "1.0",
"permissions": ["webRequest"]
}"#,
)
.unwrap();
let js = generate_bootstrap(&manifest);
assert!(js.contains("chrome.webRequest.onBeforeRequest"));
assert!(js.contains("chrome.webRequest.onBeforeSendHeaders"));
assert!(js.contains("chrome.webRequest.onCompleted"));
assert!(js.contains("chrome.webRequest.onErrorOccurred"));
}
#[test]
fn bootstrap_mv2_minimal() {
let manifest = Manifest::parse(
r#"{
"name": "MV2",
"manifest_version": 2,
"version": "1.0"
}"#,
)
.unwrap();
let js = generate_bootstrap(&manifest);
assert!(js.contains("chrome.runtime"));
assert!(js.contains("chrome.scripting"));
}
#[test]
fn bootstrap_mv3_minimal() {
let manifest = Manifest::parse(
r#"{
"name": "MV3",
"manifest_version": 3,
"version": "1.0"
}"#,
)
.unwrap();
let js = generate_bootstrap(&manifest);
assert!(js.contains("chrome.runtime"));
assert!(js.contains("chrome.scripting"));
}
}