Add font enumeration hardening, document.fonts protection, Client Hints stripping, and WebGL parameter normalization
- Font enumeration: seeded noise on offsetWidth/Height, scrollWidth/Height, clientWidth/Height - document.fonts: check() returns false, size returns 0, forEach is no-op - Client Hints: strip Sec-CH-UA/Full-Version-List, override Platform/Mobile per container - WebGL: merge PARAM_OVERRIDES into getParameter (MAX_TEXTURE_SIZE, attribs, etc.) - Clean up dead code in WebGL extended section - Test page: add Font DOM, document.fonts, and Client Hints test sections - README: update vector table (18 vectors), add about:config and testing docs
This commit is contained in:
53
README.md
53
README.md
@@ -10,6 +10,7 @@ Every website you visit is automatically placed in its own isolated container wi
|
|||||||
- **Unique fingerprints per container** — every container presents a completely different device to websites
|
- **Unique fingerprints per container** — every container presents a completely different device to websites
|
||||||
- **Auth-aware** — login redirects (e.g. YouTube to Google) stay in the originating container so authentication works seamlessly
|
- **Auth-aware** — login redirects (e.g. YouTube to Google) stay in the originating container so authentication works seamlessly
|
||||||
- **Cross-site navigation** — clicking a link to a different domain automatically switches to the correct container
|
- **Cross-site navigation** — clicking a link to a different domain automatically switches to the correct container
|
||||||
|
- **HTTP header spoofing** — User-Agent, Accept-Language, and Client Hints headers match each container's identity
|
||||||
- **Configurable** — toggle individual fingerprint vectors, whitelist domains, manage containers from the options page
|
- **Configurable** — toggle individual fingerprint vectors, whitelist domains, manage containers from the options page
|
||||||
- **Zero configuration** — install and browse, everything is automatic
|
- **Zero configuration** — install and browse, everything is automatic
|
||||||
|
|
||||||
@@ -18,17 +19,23 @@ Every website you visit is automatically placed in its own isolated container wi
|
|||||||
| Vector | Method |
|
| Vector | Method |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Canvas | Deterministic pixel noise per container seed |
|
| Canvas | Deterministic pixel noise per container seed |
|
||||||
| WebGL | Spoofed GPU vendor and renderer strings |
|
| WebGL | Spoofed GPU vendor, renderer, max parameters, and normalized extensions |
|
||||||
| AudioContext | Seeded noise on frequency and channel data |
|
| AudioContext | Seeded noise on frequency and channel data |
|
||||||
| Navigator | CPU cores, platform, languages, device memory |
|
| Navigator | CPU cores, platform, languages, device memory, oscpu |
|
||||||
| Screen | Resolution, color depth, window dimensions |
|
| Screen | Resolution, color depth, window dimensions |
|
||||||
| Timezone | getTimezoneOffset, Date.toString, Intl.DateTimeFormat |
|
| Timezone | getTimezoneOffset, Date.toString, Intl.DateTimeFormat |
|
||||||
| WebRTC | Forced relay-only ICE policy (blocks local IP leak) |
|
| WebRTC | Forced relay-only ICE policy (blocks local IP leak) |
|
||||||
| Fonts | Noise on measureText (prevents font enumeration) |
|
| Fonts | Noise on measureText + DOM element dimensions (offsetWidth/Height etc.) |
|
||||||
|
| Font API | document.fonts.check() blocked, size reports 0 |
|
||||||
| ClientRects | Sub-pixel noise on getBoundingClientRect |
|
| ClientRects | Sub-pixel noise on getBoundingClientRect |
|
||||||
| Plugins | Reports empty |
|
| Plugins | Reports empty |
|
||||||
| Battery | Always reports full/charging |
|
| Battery | Always reports full/charging |
|
||||||
| Connection | Fixed network profile |
|
| Connection | Fixed network profile |
|
||||||
|
| HTTP Headers | User-Agent, Accept-Language spoofed per container; Client Hints stripped |
|
||||||
|
| Speech Synthesis | getVoices() returns empty, voiceschanged suppressed |
|
||||||
|
| matchMedia | Screen dimension queries return spoofed values |
|
||||||
|
| Performance | performance.now() precision reduced to 0.1ms |
|
||||||
|
| Storage | navigator.storage.estimate() returns generic values |
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
@@ -47,15 +54,19 @@ Background Script
|
|||||||
├── Auto-creates containers per domain (contextualIdentities API)
|
├── Auto-creates containers per domain (contextualIdentities API)
|
||||||
├── Generates deterministic fingerprint from seed (Mulberry32 PRNG)
|
├── Generates deterministic fingerprint from seed (Mulberry32 PRNG)
|
||||||
├── Registers per-container content scripts (contentScripts.register + cookieStoreId)
|
├── Registers per-container content scripts (contentScripts.register + cookieStoreId)
|
||||||
└── Intercepts navigation to assign tabs to containers
|
├── Intercepts navigation to assign tabs to containers
|
||||||
|
└── Spoofs HTTP headers (User-Agent, Accept-Language, Client Hints) per container
|
||||||
|
|
||||||
Content Script (per container, ISOLATED world, document_start)
|
Content Script (per container, ISOLATED world, document_start)
|
||||||
└── Uses exportFunction() + wrappedJSObject to override page APIs
|
└── Uses exportFunction() + wrappedJSObject to override page APIs
|
||||||
├── Canvas, WebGL, AudioContext prototypes
|
├── Canvas, WebGL, AudioContext prototypes
|
||||||
├── Navigator, Screen properties
|
├── Navigator, Screen, Performance properties
|
||||||
├── Timezone (Date, Intl.DateTimeFormat)
|
├── Timezone (Date, Intl.DateTimeFormat)
|
||||||
├── WebRTC (RTCPeerConnection)
|
├── WebRTC (RTCPeerConnection)
|
||||||
└── Font metrics, ClientRects, Battery, Connection
|
├── Font metrics (measureText, DOM dimensions, document.fonts)
|
||||||
|
├── ClientRects, Battery, Connection, Storage
|
||||||
|
├── Speech synthesis, matchMedia
|
||||||
|
└── Plugins, mimeTypes
|
||||||
```
|
```
|
||||||
|
|
||||||
Uses Firefox's `exportFunction()` API to inject overrides from the isolated content script world directly into the page context. This bypasses Content Security Policy restrictions that block inline script injection.
|
Uses Firefox's `exportFunction()` API to inject overrides from the isolated content script world directly into the page context. This bypasses Content Security Policy restrictions that block inline script injection.
|
||||||
@@ -92,7 +103,7 @@ Right-click the toolbar icon → **Manage Extension** → **Preferences** to ope
|
|||||||
|
|
||||||
### Fingerprint Vectors
|
### Fingerprint Vectors
|
||||||
|
|
||||||
Toggle individual spoofing vectors on or off globally. All 12 vectors can be independently controlled:
|
Toggle individual spoofing vectors on or off globally. Vectors can be independently controlled:
|
||||||
|
|
||||||
Canvas, WebGL, Audio, Navigator, Screen, Timezone, WebRTC, Fonts, Client Rects, Plugins, Battery, Connection
|
Canvas, WebGL, Audio, Navigator, Screen, Timezone, WebRTC, Fonts, Client Rects, Plugins, Battery, Connection
|
||||||
|
|
||||||
@@ -113,12 +124,34 @@ Full table of all managed containers with per-container controls:
|
|||||||
- Firefox 100+ or LibreWolf
|
- Firefox 100+ or LibreWolf
|
||||||
- Containers must be enabled (`privacy.userContext.enabled = true` in `about:config`)
|
- Containers must be enabled (`privacy.userContext.enabled = true` in `about:config`)
|
||||||
|
|
||||||
|
### Recommended about:config settings
|
||||||
|
|
||||||
|
For maximum WebRTC leak protection, set these in `about:config`:
|
||||||
|
|
||||||
|
| Setting | Value | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `media.peerconnection.ice.default_address_only` | `true` | Only use default route for ICE |
|
||||||
|
| `media.peerconnection.ice.no_host` | `true` | Prevent host candidate gathering |
|
||||||
|
| `media.peerconnection.ice.proxy_only_if_behind_proxy` | `true` | Force proxy-only mode |
|
||||||
|
|
||||||
|
LibreWolf may already have some of these set by default.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
A built-in test page is included at `test/fingerprint-test.html`. To use it:
|
||||||
|
|
||||||
|
1. Load the extension via `about:debugging`
|
||||||
|
2. Add a hostname alias (e.g. `127.0.0.1 containsite-test.site` in `/etc/hosts`) — localhost is excluded from containerization
|
||||||
|
3. Start a local server: `python3 -m http.server 8888 --bind 0.0.0.0`
|
||||||
|
4. Open `http://containsite-test.site:8888/test/fingerprint-test.html` in a regular (non-private) window
|
||||||
|
5. Open the same URL in a different container tab and compare composite hashes
|
||||||
|
|
||||||
## File structure
|
## File structure
|
||||||
|
|
||||||
```
|
```
|
||||||
manifest.json MV2 extension manifest
|
manifest.json MV2 extension manifest
|
||||||
background.js Container management, navigation interception, script registration
|
background.js Container management, navigation, HTTP header spoofing
|
||||||
inject.js Fingerprint overrides (exportFunction-based)
|
inject.js Fingerprint overrides (exportFunction-based, 18 vectors)
|
||||||
lib/
|
lib/
|
||||||
prng.js Mulberry32 seeded PRNG
|
prng.js Mulberry32 seeded PRNG
|
||||||
fingerprint-gen.js Deterministic seed → device profile generator
|
fingerprint-gen.js Deterministic seed → device profile generator
|
||||||
@@ -130,6 +163,8 @@ options/
|
|||||||
options.html Full options page (opens in tab)
|
options.html Full options page (opens in tab)
|
||||||
options.css Styles
|
options.css Styles
|
||||||
options.js Vector toggles, whitelist, container management
|
options.js Vector toggles, whitelist, container management
|
||||||
|
test/
|
||||||
|
fingerprint-test.html Comprehensive fingerprint verification page
|
||||||
icons/
|
icons/
|
||||||
icon-48.png Toolbar icon
|
icon-48.png Toolbar icon
|
||||||
icon-96.png Extension icon
|
icon-96.png Extension icon
|
||||||
|
|||||||
@@ -91,7 +91,8 @@ async function buildProfileAndRegister(cookieStoreId, seed) {
|
|||||||
// Cache profile for HTTP header spoofing
|
// Cache profile for HTTP header spoofing
|
||||||
containerProfiles[cookieStoreId] = {
|
containerProfiles[cookieStoreId] = {
|
||||||
userAgent: profile.nav.userAgent,
|
userAgent: profile.nav.userAgent,
|
||||||
languages: profile.nav.languages
|
languages: profile.nav.languages,
|
||||||
|
platform: profile.nav.platform
|
||||||
};
|
};
|
||||||
|
|
||||||
await registerForContainer(cookieStoreId, profile);
|
await registerForContainer(cookieStoreId, profile);
|
||||||
@@ -450,12 +451,25 @@ browser.webRequest.onBeforeSendHeaders.addListener(
|
|||||||
if (!profile) return {};
|
if (!profile) return {};
|
||||||
|
|
||||||
const headers = details.requestHeaders;
|
const headers = details.requestHeaders;
|
||||||
for (let i = 0; i < headers.length; i++) {
|
// Map platform to Client Hints platform name
|
||||||
|
const platformMap = {
|
||||||
|
"Win32": "Windows", "Linux x86_64": "Linux", "MacIntel": "macOS"
|
||||||
|
};
|
||||||
|
const chPlatform = platformMap[profile.platform] || "Unknown";
|
||||||
|
|
||||||
|
for (let i = headers.length - 1; i >= 0; i--) {
|
||||||
const name = headers[i].name.toLowerCase();
|
const name = headers[i].name.toLowerCase();
|
||||||
if (name === "user-agent") {
|
if (name === "user-agent") {
|
||||||
headers[i].value = profile.userAgent;
|
headers[i].value = profile.userAgent;
|
||||||
} else if (name === "accept-language") {
|
} else if (name === "accept-language") {
|
||||||
headers[i].value = formatAcceptLanguage(profile.languages);
|
headers[i].value = formatAcceptLanguage(profile.languages);
|
||||||
|
} else if (name === "sec-ch-ua" || name === "sec-ch-ua-full-version-list") {
|
||||||
|
// Firefox doesn't normally send these, but strip if present
|
||||||
|
headers.splice(i, 1);
|
||||||
|
} else if (name === "sec-ch-ua-platform") {
|
||||||
|
headers[i].value = `"${chPlatform}"`;
|
||||||
|
} else if (name === "sec-ch-ua-mobile") {
|
||||||
|
headers[i].value = "?0";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { requestHeaders: headers };
|
return { requestHeaders: headers };
|
||||||
|
|||||||
88
inject.js
88
inject.js
@@ -92,6 +92,20 @@
|
|||||||
const UNMASKED_VENDOR = 0x9245;
|
const UNMASKED_VENDOR = 0x9245;
|
||||||
const UNMASKED_RENDERER = 0x9246;
|
const UNMASKED_RENDERER = 0x9246;
|
||||||
|
|
||||||
|
// Normalize key max parameters to common values to prevent GPU fingerprinting
|
||||||
|
const PARAM_OVERRIDES = {
|
||||||
|
0x0D33: 16384, // MAX_TEXTURE_SIZE
|
||||||
|
0x851C: 16384, // MAX_CUBE_MAP_TEXTURE_SIZE
|
||||||
|
0x84E8: 16384, // MAX_RENDERBUFFER_SIZE
|
||||||
|
0x8869: 16, // MAX_VERTEX_ATTRIBS
|
||||||
|
0x8872: 16, // MAX_VERTEX_TEXTURE_IMAGE_UNITS
|
||||||
|
0x8B4C: 16, // MAX_TEXTURE_IMAGE_UNITS
|
||||||
|
0x8DFB: 32, // MAX_VARYING_VECTORS
|
||||||
|
0x8DFC: 256, // MAX_VERTEX_UNIFORM_VECTORS
|
||||||
|
0x8DFD: 512, // MAX_FRAGMENT_UNIFORM_VECTORS
|
||||||
|
0x80A9: 16, // MAX_SAMPLES
|
||||||
|
};
|
||||||
|
|
||||||
function patchWebGL(protoName) {
|
function patchWebGL(protoName) {
|
||||||
const pageProto = pageWindow[protoName];
|
const pageProto = pageWindow[protoName];
|
||||||
if (!pageProto) return;
|
if (!pageProto) return;
|
||||||
@@ -102,6 +116,11 @@
|
|||||||
exportFunction(function(pname) {
|
exportFunction(function(pname) {
|
||||||
if (pname === UNMASKED_VENDOR) return CONFIG.webgl.vendor;
|
if (pname === UNMASKED_VENDOR) return CONFIG.webgl.vendor;
|
||||||
if (pname === UNMASKED_RENDERER) return CONFIG.webgl.renderer;
|
if (pname === UNMASKED_RENDERER) return CONFIG.webgl.renderer;
|
||||||
|
if (PARAM_OVERRIDES[pname] !== undefined) {
|
||||||
|
// Return the normalized value, but never exceed the real GPU's capability
|
||||||
|
const real = origGetParam.call(this, pname);
|
||||||
|
return (typeof real === "number") ? Math.min(real, PARAM_OVERRIDES[pname]) : real;
|
||||||
|
}
|
||||||
return origGetParam.call(this, pname);
|
return origGetParam.call(this, pname);
|
||||||
}, pageProto.prototype, { defineAs: "getParameter" });
|
}, pageProto.prototype, { defineAs: "getParameter" });
|
||||||
}
|
}
|
||||||
@@ -443,6 +462,44 @@
|
|||||||
|
|
||||||
return metrics;
|
return metrics;
|
||||||
}, pageWindow.CanvasRenderingContext2D.prototype, { defineAs: "measureText" });
|
}, pageWindow.CanvasRenderingContext2D.prototype, { defineAs: "measureText" });
|
||||||
|
|
||||||
|
// --- DOM Element Dimension Noise ---
|
||||||
|
// Font enumeration measures offsetWidth/Height of test spans to detect installed fonts.
|
||||||
|
// Adding seeded noise prevents consistent dimension-based font fingerprinting.
|
||||||
|
const fontDimProps = ["offsetWidth", "offsetHeight", "scrollWidth", "scrollHeight", "clientWidth", "clientHeight"];
|
||||||
|
for (const prop of fontDimProps) {
|
||||||
|
const origDesc = Object.getOwnPropertyDescriptor(window.HTMLElement.prototype, prop);
|
||||||
|
if (origDesc && origDesc.get) {
|
||||||
|
const origGet = origDesc.get;
|
||||||
|
Object.defineProperty(pageWindow.HTMLElement.prototype, prop, {
|
||||||
|
get: exportFunction(function() {
|
||||||
|
const val = origGet.call(this);
|
||||||
|
return val + (fontRng() - 0.5) * 0.3;
|
||||||
|
}, pageWindow),
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- document.fonts (FontFaceSet) API Protection ---
|
||||||
|
// document.fonts.check() directly reveals installed fonts; size/iterators expose count.
|
||||||
|
if (pageWindow.document.fonts) {
|
||||||
|
try {
|
||||||
|
Object.defineProperty(pageWindow.document.fonts, "check", {
|
||||||
|
value: exportFunction(function() { return false; }, pageWindow),
|
||||||
|
configurable: true, enumerable: true
|
||||||
|
});
|
||||||
|
Object.defineProperty(pageWindow.document.fonts, "size", {
|
||||||
|
get: exportFunction(function() { return 0; }, pageWindow),
|
||||||
|
configurable: true, enumerable: true
|
||||||
|
});
|
||||||
|
Object.defineProperty(pageWindow.document.fonts, "forEach", {
|
||||||
|
value: exportFunction(function() {}, pageWindow),
|
||||||
|
configurable: true, enumerable: true
|
||||||
|
});
|
||||||
|
} catch(e) {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -580,17 +637,15 @@
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
// WEBGL EXTENDED FINGERPRINT PROTECTION
|
// WEBGL EXTENDED FINGERPRINT PROTECTION
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Beyond vendor/renderer, WebGL exposes max parameters and extensions
|
// Normalize getSupportedExtensions to a common baseline set
|
||||||
// that vary per GPU and can be used for fingerprinting.
|
|
||||||
|
|
||||||
if (vectorEnabled("webgl")) {
|
if (vectorEnabled("webgl")) {
|
||||||
function patchWebGLExtended(protoName) {
|
function patchWebGLExtensions(protoName) {
|
||||||
const pageProto = pageWindow[protoName];
|
const pageProto = pageWindow[protoName];
|
||||||
if (!pageProto) return;
|
if (!pageProto) return;
|
||||||
const origProto = window[protoName];
|
const origProto = window[protoName];
|
||||||
if (!origProto) return;
|
if (!origProto) return;
|
||||||
|
|
||||||
// Spoof getSupportedExtensions to return a consistent set
|
|
||||||
const origGetExtensions = origProto.prototype.getSupportedExtensions;
|
const origGetExtensions = origProto.prototype.getSupportedExtensions;
|
||||||
const BASELINE_EXTENSIONS = [
|
const BASELINE_EXTENSIONS = [
|
||||||
"ANGLE_instanced_arrays", "EXT_blend_minmax", "EXT_color_buffer_half_float",
|
"ANGLE_instanced_arrays", "EXT_blend_minmax", "EXT_color_buffer_half_float",
|
||||||
@@ -606,34 +661,13 @@
|
|||||||
exportFunction(function() {
|
exportFunction(function() {
|
||||||
const real = origGetExtensions.call(this);
|
const real = origGetExtensions.call(this);
|
||||||
if (!real) return real;
|
if (!real) return real;
|
||||||
// Return intersection of real and baseline — only report extensions
|
|
||||||
// that are in both sets to normalize across GPUs
|
|
||||||
const filtered = BASELINE_EXTENSIONS.filter(e => real.includes(e));
|
const filtered = BASELINE_EXTENSIONS.filter(e => real.includes(e));
|
||||||
return cloneInto(filtered, pageWindow);
|
return cloneInto(filtered, pageWindow);
|
||||||
}, pageProto.prototype, { defineAs: "getSupportedExtensions" });
|
}, pageProto.prototype, { defineAs: "getSupportedExtensions" });
|
||||||
|
|
||||||
// Normalize key max parameters to common values
|
|
||||||
const origGetParam = origProto.prototype.getParameter;
|
|
||||||
const PARAM_OVERRIDES = {
|
|
||||||
0x0D33: 16384, // MAX_TEXTURE_SIZE
|
|
||||||
0x851C: 16384, // MAX_CUBE_MAP_TEXTURE_SIZE
|
|
||||||
0x84E8: 16384, // MAX_RENDERBUFFER_SIZE
|
|
||||||
0x8869: 16, // MAX_VERTEX_ATTRIBS
|
|
||||||
0x8872: 16, // MAX_VERTEX_TEXTURE_IMAGE_UNITS
|
|
||||||
0x8B4C: 16, // MAX_TEXTURE_IMAGE_UNITS
|
|
||||||
0x8DFB: 32, // MAX_VARYING_VECTORS
|
|
||||||
0x8DFC: 256, // MAX_VERTEX_UNIFORM_VECTORS
|
|
||||||
0x8DFD: 512, // MAX_FRAGMENT_UNIFORM_VECTORS
|
|
||||||
0x80A9: 16, // MAX_SAMPLES (for multisampling)
|
|
||||||
};
|
|
||||||
// Don't re-override getParameter if webgl vendor/renderer already did it
|
|
||||||
// Instead, extend the existing override with additional parameter checks
|
|
||||||
// (The webgl section above already overrides getParameter, but only for
|
|
||||||
// UNMASKED_VENDOR/RENDERER. We need to patch at the origProto level too.)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
patchWebGLExtended("WebGLRenderingContext");
|
patchWebGLExtensions("WebGLRenderingContext");
|
||||||
patchWebGLExtended("WebGL2RenderingContext");
|
patchWebGLExtensions("WebGL2RenderingContext");
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|||||||
@@ -182,10 +182,29 @@
|
|||||||
<div class="section-body" id="storage-results"></div>
|
<div class="section-body" id="storage-results"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Font Enumeration (DOM) -->
|
||||||
|
<div class="section" id="sec-fontdom">
|
||||||
|
<div class="section-header"><span class="status-dot pending" id="dot-fontdom"></span><h2>Font Enumeration (DOM Dimensions)</h2></div>
|
||||||
|
<div class="section-body" id="fontdom-results"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- document.fonts -->
|
||||||
|
<div class="section" id="sec-docfonts">
|
||||||
|
<div class="section-header"><span class="status-dot pending" id="dot-docfonts"></span><h2>document.fonts API</h2></div>
|
||||||
|
<div class="section-body" id="docfonts-results"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Client Hints -->
|
||||||
|
<div class="section" id="sec-clienthints">
|
||||||
|
<div class="section-header"><span class="status-dot pending" id="dot-clienthints"></span><h2>Client Hints Headers</h2></div>
|
||||||
|
<div class="section-body" id="clienthints-results"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button class="copy-btn" onclick="copyReport()">Copy Full Report to Clipboard</button>
|
<button class="copy-btn" onclick="copyReport()">Copy Full Report to Clipboard</button>
|
||||||
|
|
||||||
<canvas id="test-canvas" width="300" height="60"></canvas>
|
<canvas id="test-canvas" width="300" height="60"></canvas>
|
||||||
<span id="rect-probe" style="position:absolute;visibility:hidden;font-size:16px;">ABCDEFghijklmnop</span>
|
<span id="rect-probe" style="position:absolute;visibility:hidden;font-size:16px;">ABCDEFghijklmnop</span>
|
||||||
|
<span id="fontdom-probe" style="position:absolute;visibility:hidden;font-size:72px;font-family:monospace;">mmmmmmmmmmmmmmm</span>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Utility: simple hash for display (works on HTTP — no crypto.subtle needed)
|
// Utility: simple hash for display (works on HTTP — no crypto.subtle needed)
|
||||||
@@ -837,6 +856,131 @@ async function testStorage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Font Enumeration (DOM) ──
|
||||||
|
function testFontDOM() {
|
||||||
|
const el = document.getElementById("fontdom-results");
|
||||||
|
const probe = document.getElementById("fontdom-probe");
|
||||||
|
|
||||||
|
// Measure element dimensions — should have noise if spoofed
|
||||||
|
const w = probe.offsetWidth;
|
||||||
|
const h = probe.offsetHeight;
|
||||||
|
const sw = probe.scrollWidth;
|
||||||
|
const sh = probe.scrollHeight;
|
||||||
|
const cw = probe.clientWidth;
|
||||||
|
const ch = probe.clientHeight;
|
||||||
|
|
||||||
|
const vals = {
|
||||||
|
"offsetWidth": w, "offsetHeight": h,
|
||||||
|
"scrollWidth": sw, "scrollHeight": sh,
|
||||||
|
"clientWidth": cw, "clientHeight": ch
|
||||||
|
};
|
||||||
|
report.fontdom = vals;
|
||||||
|
|
||||||
|
let html = "";
|
||||||
|
for (const [k, v] of Object.entries(vals)) {
|
||||||
|
const hasFrac = v % 1 !== 0;
|
||||||
|
html += row(k, v.toFixed(6), hasFrac ? "spoofed" : "");
|
||||||
|
}
|
||||||
|
el.innerHTML = html;
|
||||||
|
|
||||||
|
// If noise is applied, at least some values will have fractional parts
|
||||||
|
const anyFrac = Object.values(vals).some(v => v % 1 !== 0);
|
||||||
|
sectionStatus.fontdom = anyFrac ? "pass" : "warn";
|
||||||
|
setDot("dot-fontdom", sectionStatus.fontdom);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── document.fonts API ──
|
||||||
|
function testDocFonts() {
|
||||||
|
const el = document.getElementById("docfonts-results");
|
||||||
|
if (!document.fonts) {
|
||||||
|
report.docfonts = { status: "API not available" };
|
||||||
|
el.innerHTML = row("Status", "document.fonts not available", "");
|
||||||
|
sectionStatus.docfonts = "warn";
|
||||||
|
setDot("dot-docfonts", "warn");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size = document.fonts.size;
|
||||||
|
const checkArial = document.fonts.check("16px Arial");
|
||||||
|
const checkMono = document.fonts.check("16px monospace");
|
||||||
|
|
||||||
|
const vals = {
|
||||||
|
"document.fonts.size": size,
|
||||||
|
"check('16px Arial')": checkArial,
|
||||||
|
"check('16px monospace')": checkMono,
|
||||||
|
};
|
||||||
|
report.docfonts = vals;
|
||||||
|
|
||||||
|
let html = "";
|
||||||
|
html += row("document.fonts.size", size, size === 0 ? "spoofed" : "real");
|
||||||
|
html += row("check('16px Arial')", checkArial, !checkArial ? "spoofed" : "real");
|
||||||
|
html += row("check('16px monospace')", checkMono, !checkMono ? "spoofed" : "real");
|
||||||
|
el.innerHTML = html;
|
||||||
|
|
||||||
|
const spoofed = size === 0 && !checkArial && !checkMono;
|
||||||
|
sectionStatus.docfonts = spoofed ? "pass" : "warn";
|
||||||
|
setDot("dot-docfonts", sectionStatus.docfonts);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Client Hints Headers ──
|
||||||
|
async function testClientHints() {
|
||||||
|
const el = document.getElementById("clienthints-results");
|
||||||
|
try {
|
||||||
|
await withTimeout(async () => {
|
||||||
|
// Try fetching httpbin.org to see what headers are actually sent
|
||||||
|
let html = "";
|
||||||
|
try {
|
||||||
|
const resp = await fetch("https://httpbin.org/headers", {
|
||||||
|
method: "GET",
|
||||||
|
mode: "cors"
|
||||||
|
});
|
||||||
|
const data = await resp.json();
|
||||||
|
const headers = data.headers || {};
|
||||||
|
|
||||||
|
const ua = headers["User-Agent"] || "(not sent)";
|
||||||
|
const al = headers["Accept-Language"] || "(not sent)";
|
||||||
|
const chUA = headers["Sec-Ch-Ua"] || "(not sent)";
|
||||||
|
const chPlatform = headers["Sec-Ch-Ua-Platform"] || "(not sent)";
|
||||||
|
const chMobile = headers["Sec-Ch-Ua-Mobile"] || "(not sent)";
|
||||||
|
|
||||||
|
report.clienthints = {
|
||||||
|
"HTTP User-Agent": ua,
|
||||||
|
"HTTP Accept-Language": al,
|
||||||
|
"Sec-CH-UA": chUA,
|
||||||
|
"Sec-CH-UA-Platform": chPlatform,
|
||||||
|
"Sec-CH-UA-Mobile": chMobile
|
||||||
|
};
|
||||||
|
|
||||||
|
html += row("HTTP User-Agent", ua);
|
||||||
|
html += row("HTTP Accept-Language", al);
|
||||||
|
html += row("Sec-CH-UA", chUA, chUA === "(not sent)" ? "spoofed" : "");
|
||||||
|
html += row("Sec-CH-UA-Platform", chPlatform, chPlatform === "(not sent)" ? "spoofed" : "");
|
||||||
|
html += row("Sec-CH-UA-Mobile", chMobile, chMobile === "(not sent)" ? "spoofed" : "");
|
||||||
|
|
||||||
|
// Check if HTTP UA matches JS UA
|
||||||
|
const jsUA = navigator.userAgent;
|
||||||
|
const uaMatch = ua.includes("Firefox") && jsUA.includes("Firefox");
|
||||||
|
html += row("JS/HTTP UA coherent", uaMatch ? "yes" : "MISMATCH", uaMatch ? "spoofed" : "real");
|
||||||
|
|
||||||
|
sectionStatus.clienthints = uaMatch ? "pass" : "fail";
|
||||||
|
} catch(e) {
|
||||||
|
report.clienthints = { note: "httpbin.org fetch failed (CORS/network)" };
|
||||||
|
html += row("httpbin.org fetch", "Failed: " + e.message);
|
||||||
|
html += row("Note", "Client Hints are stripped by the extension if present");
|
||||||
|
sectionStatus.clienthints = "warn";
|
||||||
|
}
|
||||||
|
|
||||||
|
el.innerHTML = html;
|
||||||
|
setDot("dot-clienthints", sectionStatus.clienthints);
|
||||||
|
}, 5000);
|
||||||
|
} catch(e) {
|
||||||
|
report.clienthints = { error: e.message };
|
||||||
|
el.innerHTML = row("Error", e.message);
|
||||||
|
sectionStatus.clienthints = "warn";
|
||||||
|
setDot("dot-clienthints", "warn");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Summary ──
|
// ── Summary ──
|
||||||
function updateSummary() {
|
function updateSummary() {
|
||||||
let pass = 0, fail = 0, warn = 0;
|
let pass = 0, fail = 0, warn = 0;
|
||||||
@@ -914,6 +1058,8 @@ async function runAll() {
|
|||||||
try { testSpeech(); } catch(e) { console.error("speech test:", e); }
|
try { testSpeech(); } catch(e) { console.error("speech test:", e); }
|
||||||
try { testMatchMedia(); } catch(e) { console.error("matchmedia test:", e); }
|
try { testMatchMedia(); } catch(e) { console.error("matchmedia test:", e); }
|
||||||
try { testPerf(); } catch(e) { console.error("perf test:", e); }
|
try { testPerf(); } catch(e) { console.error("perf test:", e); }
|
||||||
|
try { testFontDOM(); } catch(e) { console.error("fontdom test:", e); }
|
||||||
|
try { testDocFonts(); } catch(e) { console.error("docfonts test:", e); }
|
||||||
|
|
||||||
// Async tests — run in parallel with individual error handling
|
// Async tests — run in parallel with individual error handling
|
||||||
await Promise.allSettled([
|
await Promise.allSettled([
|
||||||
@@ -923,6 +1069,7 @@ async function runAll() {
|
|||||||
testWebRTC(),
|
testWebRTC(),
|
||||||
testHeaders(),
|
testHeaders(),
|
||||||
testStorage(),
|
testStorage(),
|
||||||
|
testClientHints(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
updateSummary();
|
updateSummary();
|
||||||
|
|||||||
Reference in New Issue
Block a user