mirror of
https://github.com/mmis1000/SofaNav.git
synced 2026-10-18 17:01:07 +02:00
Follow-up to E29. When the userscript runs in an isolated world (Tampermonkey @sandbox DOM, Violentmonkey @inject-into content, checked via GM_info), frame commands travel through the manager's per-script storage (GM_setValue + GM_addValueChangeListener on random per-instance keys), which pages can't write. Without isolation it stays same-origin only: in page mode the attacker page can fake navigator.getGamepads for SofaNav in its own top frame (shown by a negative control). Also: activate() passes document.defaultView as the event view, since userscript managers may wrap window (PointerEvent threw in VM page mode). test:security now installs the userscript in a pinned, sha256-checked Violentmonkey and adds a fake-controller attack. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
55 lines
2.7 KiB
JavaScript
55 lines
2.7 KiB
JavaScript
// Downloads and unpacks the Violentmonkey release the userscript security test runs under.
|
|
// Pinned by version and sha256, so CI tests the same build every time.
|
|
// Output: .cache/violentmonkey-<version>/ (an unpacked MV3 extension). Prints that path.
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const zlib = require('zlib');
|
|
const crypto = require('crypto');
|
|
|
|
const VERSION = '2.49.0';
|
|
const URL = `https://github.com/violentmonkey/violentmonkey/releases/download/v${VERSION}/Violentmonkey-mv3-v${VERSION}.zip`;
|
|
const SHA256 = '3fa676c803a9698453ed23dfe496d4bc29a4bec52f6a95e55c43c2955921f8ee';
|
|
const CACHE = path.join(__dirname, '..', '.cache');
|
|
const ZIP = path.join(CACHE, 'dl', `Violentmonkey-mv3-v${VERSION}.zip`);
|
|
const OUT = path.join(CACHE, `violentmonkey-${VERSION}`);
|
|
|
|
// Minimal zip reader (stored and deflated entries, via the central directory).
|
|
function unzip(buf, dir) {
|
|
let eocd = buf.length - 22;
|
|
while (eocd >= 0 && buf.readUInt32LE(eocd) !== 0x06054b50) eocd--;
|
|
if (eocd < 0) throw new Error('not a zip');
|
|
const count = buf.readUInt16LE(eocd + 10);
|
|
let p = buf.readUInt32LE(eocd + 16);
|
|
for (let i = 0; i < count; i++) {
|
|
const method = buf.readUInt16LE(p + 10), csize = buf.readUInt32LE(p + 20);
|
|
const nlen = buf.readUInt16LE(p + 28), xlen = buf.readUInt16LE(p + 30), clen = buf.readUInt16LE(p + 32);
|
|
const local = buf.readUInt32LE(p + 42);
|
|
const name = buf.toString('utf8', p + 46, p + 46 + nlen);
|
|
p += 46 + nlen + xlen + clen;
|
|
const target = path.resolve(dir, name);
|
|
if (!target.startsWith(path.resolve(dir) + path.sep)) throw new Error(`unsafe path in zip: ${name}`);
|
|
if (name.endsWith('/')) { fs.mkdirSync(target, { recursive: true }); continue; }
|
|
const start = local + 30 + buf.readUInt16LE(local + 26) + buf.readUInt16LE(local + 28);
|
|
const data = buf.subarray(start, start + csize);
|
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
fs.writeFileSync(target, method === 0 ? data : zlib.inflateRawSync(data));
|
|
}
|
|
}
|
|
|
|
(async () => {
|
|
if (fs.existsSync(path.join(OUT, 'manifest.json'))) { console.log(OUT); return; }
|
|
let buf = fs.existsSync(ZIP) ? fs.readFileSync(ZIP) : null;
|
|
if (!buf) {
|
|
const res = await fetch(URL);
|
|
if (!res.ok) throw new Error(`download failed: ${res.status}`);
|
|
buf = Buffer.from(await res.arrayBuffer());
|
|
}
|
|
const hash = crypto.createHash('sha256').update(buf).digest('hex');
|
|
if (hash !== SHA256) throw new Error(`sha256 mismatch: ${hash}`);
|
|
fs.mkdirSync(path.dirname(ZIP), { recursive: true });
|
|
fs.writeFileSync(ZIP, buf);
|
|
fs.rmSync(OUT, { recursive: true, force: true });
|
|
unzip(buf, OUT);
|
|
console.log(OUT);
|
|
})().catch((e) => { console.error(e.message); process.exit(1); });
|