Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 28 additions & 8 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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));
Expand All @@ -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) {
Expand Down Expand Up @@ -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<string, number | boolean> = {
"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) {
Expand Down
63 changes: 63 additions & 0 deletions test/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,66 @@ describe("launchOptions seeding", () => {
).rejects.toThrow("Unknown property audio:seed in config");
});
});

describe("launchOptions dropped-property filtering", () => {
const readConfig = (env: Record<string, unknown>) =>
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");
});
});