From 65b1caaba30d1f478a4086a3ab4e495f85c4d515 Mon Sep 17 00:00:00 2001 From: goodchang77 Date: Sun, 4 Oct 2026 11:50:57 +0800 Subject: [PATCH] fix: skip generated config properties the installed Camoufox dropped Camoufox v156 removed several config properties that the library still generates: BrowserForge-mapped navigator.product/appCodeName/appName, battery:*, screen.pageXOffset/pageYOffset, plus the hardcoded window.history.length and canvas:aaOffset/aaCapOffset. launch() then fails with 'Unknown property in config' from validateConfig. Load the installed build's properties.json before assembling the config and filter the fingerprint-derived keys against it; guard the hardcoded window.history.length and canvas anti-fingerprinting additions the same way. Caller-supplied config keys are untouched, so validateConfig still catches user typos (covered by a new test). On Camoufox v156.0.1-beta.34: test/basics.test.ts goes from 12/15 failing (browser cannot launch) to 11/15 passing; the remaining locale test failure is a separate v156 behavioral issue. The three 'seeding' tests in test/utils.test.ts pin against the installed browser's schema and fail on v156 with or without this change. --- src/utils.ts | 36 ++++++++++++++++++++------ test/utils.test.ts | 63 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 8 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 26bc9ce..6cc9808 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -676,8 +676,20 @@ export async function launchOptions({ } } - // Inject the fingerprint into the config - mergeInto(config, fromBrowserforge(fingerprint, ff_version_str)); + // Load the installed build's property schema up front: BrowserForge's + // mapping and the hardcoded additions below can target properties a newer + // Camoufox has dropped (v156 removed navigator.product/appCodeName/appName, + // battery:*, screen.pageXOffset/pageYOffset, window.history.length and + // canvas:aaOffset/aaCapOffset). Filter what WE generate so validateConfig + // only ever throws on caller-supplied mistakes, never on our own output. + const knownProperties = loadProperties(executable_path); + const fingerprintConfig = fromBrowserforge(fingerprint, ff_version_str); + for (const key of Object.keys(fingerprintConfig)) { + if (!(key in knownProperties)) { + delete fingerprintConfig[key]; + } + } + mergeInto(config, fingerprintConfig); // Add seeds (BrowserForge doesn't generate these). Mirrors fingerprints.py, // which seeds these right after from_browserforge() with setdefault. Range is @@ -690,7 +702,6 @@ export async function launchOptions({ // builds, so seed only what the installed browser's schema knows. const randint = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min; - const knownProperties = loadProperties(executable_path); for (const seed of ["fonts:spacing_seed", "audio:seed", "canvas:seed"]) { if (seed in knownProperties) { setInto(config, seed, randint(1, 4_294_967_295)); @@ -699,8 +710,11 @@ export async function launchOptions({ const targetOS = getTargetOS(config); - // Set a random window.history.length - setInto(config, "window.history.length", Math.floor(Math.random() * 5) + 1); + // Set a random window.history.length (only when the installed build knows + // it — removed in Camoufox v156) + if ("window.history.length" in knownProperties) { + setInto(config, "window.history.length", Math.floor(Math.random() * 5) + 1); + } // Update fonts list if (fonts) { @@ -813,11 +827,17 @@ export async function launchOptions({ }); } - // Canvas anti-fingerprinting - mergeInto(config, { + // Canvas anti-fingerprinting (only properties the installed build knows — + // canvas:* was dropped in Camoufox v156) + const canvasAntiFingerprinting: Record = { "canvas:aaOffset": Math.floor(Math.random() * 101) - 50, // nosec "canvas:aaCapOffset": true, - }); + }; + for (const [key, value] of Object.entries(canvasAntiFingerprinting)) { + if (key in knownProperties) { + config[key] = value; + } + } // Cache previous pages, requests, etc (uses more memory) if (enable_cache) { diff --git a/test/utils.test.ts b/test/utils.test.ts index a53ae3e..25699b9 100644 --- a/test/utils.test.ts +++ b/test/utils.test.ts @@ -112,3 +112,66 @@ describe("launchOptions seeding", () => { ).rejects.toThrow("Unknown property audio:seed in config"); }); }); + +describe("launchOptions dropped-property filtering", () => { + const readConfig = (env: Record) => + JSON.parse( + Object.keys(env) + .filter((k) => k.startsWith("CAMOU_CONFIG_")) + .sort() + .map((k) => env[k]) + .join(""), + ); + + // Simulates a newer browser whose properties.json no longer declares the + // properties BrowserForge/hardcoded additions target (Camoufox v156 dropped + // navigator.product/appCodeName/appName, battery:*, screen.pageXOffset, + // window.history.length and canvas:aaOffset/aaCapOffset). + const DROPPED = [ + "navigator.product", + "navigator.appCodeName", + "navigator.appName", + "battery:charging", + "battery:chargingTime", + "battery:dischargingTime", + "screen.pageXOffset", + "screen.pageYOffset", + "window.history.length", + "canvas:aaOffset", + "canvas:aaCapOffset", + ]; + const newerBrowserDir = () => { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "camoufox-newer-")); + const properties = JSON.parse( + fs.readFileSync(getPath("properties.json"), "utf-8"), + ).filter( + (p: { property: string }) => !DROPPED.includes(p.property), + ); + fs.writeFileSync( + path.join(dir, "properties.json"), + JSON.stringify(properties), + ); + return path.join(dir, "camoufox-bin"); + }; + + test("generated config omits properties the installed browser dropped", async () => { + const { env } = await launchOptions({ + headless: true, + executable_path: newerBrowserDir(), + }); + const config = readConfig(env); + for (const key of DROPPED) { + expect(config).not.toHaveProperty(key); + } + }); + + test("still rejects an explicitly passed dropped property", async () => { + await expect( + launchOptions({ + headless: true, + executable_path: newerBrowserDir(), + config: { "navigator.product": "Gecko" }, + }), + ).rejects.toThrow("Unknown property navigator.product in config"); + }); +});