@mochi.js/inject
Advanced tools
| /** | ||
| * Unit: performance-timing module — `PerformanceNavigationTiming` shim. | ||
| * | ||
| * Empirically discovered leak: Chrome launched via `--remote-debugging-pipe` | ||
| * (mochi's path) sometimes emits navigation entries with `dns: 0`, `tcp: 0`, | ||
| * `nextHopProtocol: ""` even on cold loads — a known headless tell that | ||
| * FPJS's tampering ML reads. The module wraps each navigation entry in a | ||
| * Proxy that injects realistic handshake durations only when the live | ||
| * values are zero. | ||
| * | ||
| * The sandbox in `sandbox.ts` doesn't stand up a real `performance` object | ||
| * with navigation entries, so these tests assert the SHAPE of the emitted | ||
| * JS — the same pattern `phase07-modules.test.ts` uses for webgpu / network | ||
| * info / etc. Runtime semantics are exercised by the harness E2E gate | ||
| * against real Chromium. | ||
| * | ||
| */ | ||
| import { describe, expect, it } from "bun:test"; | ||
| import { emitPerformanceTimingModule } from "../modules/performance-timing"; | ||
| import { FIXTURE_MATRIX } from "./fixtures"; | ||
| describe("performance-timing — PerformanceNavigationTiming shim", () => { | ||
| it("emits a Proxy-wrapping override for getEntriesByType('navigation')", () => { | ||
| const code = emitPerformanceTimingModule(FIXTURE_MATRIX); | ||
| expect(code).toContain("performance-timing spoof"); | ||
| expect(code).toContain("getEntriesByType"); | ||
| expect(code).toContain("getEntries"); | ||
| expect(code).toContain("getEntriesByName"); | ||
| expect(code).toContain('entry.entryType !== "navigation"'); | ||
| expect(code).toContain("new Proxy(entry,"); | ||
| }); | ||
| it("only patches the four leaky fields; other props pass through Reflect.get", () => { | ||
| const code = emitPerformanceTimingModule(FIXTURE_MATRIX); | ||
| expect(code).toContain('prop === "domainLookupEnd"'); | ||
| expect(code).toContain('prop === "connectEnd"'); | ||
| expect(code).toContain('prop === "secureConnectionStart"'); | ||
| expect(code).toContain('prop === "nextHopProtocol"'); | ||
| expect(code).toContain("Reflect.get(target, prop, receiver)"); | ||
| }); | ||
| it("uses idempotent patching — only injects when end <= start", () => { | ||
| const code = emitPerformanceTimingModule(FIXTURE_MATRIX); | ||
| // domainLookupEnd: only adds DNS_MS when end <= start (i.e. zero/coalesced) | ||
| expect(code).toMatch(/\(e <= s\) \? s \+ DNS_MS : e/); | ||
| // connectEnd similarly | ||
| expect(code).toMatch(/\(ce <= cs\) \? cs \+ TCP_MS \+ TLS_MS : ce/); | ||
| }); | ||
| it("provides a toJSON override so JSON.stringify(entry) sees the patched values", () => { | ||
| const code = emitPerformanceTimingModule(FIXTURE_MATRIX); | ||
| expect(code).toContain('prop === "toJSON"'); | ||
| expect(code).toContain("orig.domainLookupEnd"); | ||
| expect(code).toContain("orig.nextHopProtocol"); | ||
| }); | ||
| it("derives TCP/TLS budgets from matrix.uaCh.connection.rtt when present", () => { | ||
| const matrix = { | ||
| ...FIXTURE_MATRIX, | ||
| uaCh: { | ||
| ...FIXTURE_MATRIX.uaCh, | ||
| connection: JSON.stringify({ rtt: 100, downlink: 10 }), | ||
| }, | ||
| }; | ||
| const code = emitPerformanceTimingModule(matrix); | ||
| // rtt=100 → tcp = round(100 * 0.55) = 55ms, tls = round(100 * 0.1) = 10ms | ||
| expect(code).toContain("var TCP_MS = 55;"); | ||
| expect(code).toContain("var TLS_MS = 10;"); | ||
| }); | ||
| it("falls back to safe defaults when matrix.uaCh.connection is missing", () => { | ||
| const matrix = { | ||
| ...FIXTURE_MATRIX, | ||
| uaCh: { ...FIXTURE_MATRIX.uaCh, connection: "" }, | ||
| }; | ||
| const code = emitPerformanceTimingModule(matrix); | ||
| // rtt absent → baseRtt = 50ms → tcp = 28ms, tls = 5ms (the values | ||
| // empirically observed on real Chrome on a real Aixit Frankfurt | ||
| // server, suspect score 8 — see investigation 2026-05-09). | ||
| expect(code).toContain("var TCP_MS = 28;"); | ||
| expect(code).toContain("var TLS_MS = 5;"); | ||
| }); | ||
| it("clamps absurd RTT values so misconfigured matrices don't produce slow handshakes", () => { | ||
| const matrix = { | ||
| ...FIXTURE_MATRIX, | ||
| uaCh: { | ||
| ...FIXTURE_MATRIX.uaCh, | ||
| connection: JSON.stringify({ rtt: 5000 }), | ||
| }, | ||
| }; | ||
| const code = emitPerformanceTimingModule(matrix); | ||
| // rtt clamped to 200 → tcp = round(200 * 0.55) = 110ms | ||
| expect(code).toContain("var TCP_MS = 110;"); | ||
| }); | ||
| it("hardcodes nextHopProtocol fallback to h2", () => { | ||
| const code = emitPerformanceTimingModule(FIXTURE_MATRIX); | ||
| expect(code).toContain('var DEFAULT_PROTOCOL = "h2";'); | ||
| }); | ||
| }); |
| /** | ||
| * Spoof module: `PerformanceNavigationTiming`. | ||
| * | ||
| * Reads from the matrix: | ||
| * - `matrix.uaCh.connection` (R-037) — `{rtt, downlink, ...}`. The `rtt` | ||
| * value seeds a plausible TCP handshake duration; absent → defaults. | ||
| * | ||
| * **What this fixes.** Chrome launched with `--remote-debugging-pipe` (the | ||
| * mochi launch path) and certain headless / virtualised network paths emit | ||
| * navigation entries with `dns: 0`, `tcp: 0`, `nextHopProtocol: ""` even on | ||
| * fresh cold loads — the connection-establishment phases are coalesced or | ||
| * never populated. That triad is a well-known headless tell. Real Chrome on | ||
| * a real cold load shows `dns ≈ 20-50ms`, `tcp ≈ 20-40ms`, and | ||
| * `nextHopProtocol = "h2"` (or "h3" / "http/1.1") for HTTPS/2 origins. | ||
| * | ||
| * **Strategy.** Wrap each `navigation` entry returned by | ||
| * `performance.getEntriesByType("navigation")` and `performance.getEntries()` | ||
| * in a `Proxy` that overrides only the fields known to leak (domainLookupEnd, | ||
| * connectEnd, secureConnectionStart, nextHopProtocol). Every other property | ||
| * (responseStart, responseEnd, transferSize, etc.) passes through unchanged | ||
| * so cache / load-time fields stay accurate. `instanceof | ||
| * PerformanceNavigationTiming` checks pass through the proxy transparently. | ||
| * | ||
| * Idempotence: only patches when the live entry has the leaky shape | ||
| * (start === end for the relevant phase). If Chrome populated real values | ||
| * (e.g. on a non-CDP launch path) the proxy returns them unchanged. | ||
| * | ||
| * Determinism: dns + tcp values derive from a constant seed (kept simple | ||
| * for v1 — no per-call PRNG since the entry is queried multiple times by | ||
| * the same probe and must return stable values). | ||
| * | ||
| * @see PLAN.md §9.6 (timing precision philosophy — same-engine v1 keeps | ||
| * Chrome's natural coarsening; this module only fixes the | ||
| * pipe-mode-specific zero-handshake leak, not timer precision). | ||
| */ | ||
| import type { MatrixV1 } from "@mochi.js/consistency"; | ||
| interface Connection { | ||
| readonly rtt?: number; | ||
| } | ||
| function tryParse<T>(s: unknown): T | null { | ||
| if (typeof s !== "string" || s.length === 0) return null; | ||
| try { | ||
| return JSON.parse(s) as T; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
| export function emitPerformanceTimingModule(matrix: MatrixV1): string { | ||
| const conn = tryParse<Connection>(matrix.uaCh.connection) ?? {}; | ||
| // RTT seeds connect time; cap at 200ms so absurd values from misconfigured | ||
| // matrices don't produce comically slow handshakes (200ms TCP+TLS is still | ||
| // within real residential-broadband range). | ||
| const baseRtt = typeof conn.rtt === "number" && conn.rtt > 0 ? Math.min(conn.rtt, 200) : 50; | ||
| // DNS lookup is independent of RTT — pick a stable value in real-Chrome range. | ||
| const dnsMs = 30; | ||
| // TCP connect is roughly one RTT for a 3-way handshake on a warm cache. | ||
| const tcpMs = Math.max(20, Math.round(baseRtt * 0.55)); | ||
| // TLS adds ~1 RTT after TCP for TLS 1.3 1-RTT handshake. | ||
| const tlsMs = Math.max(5, Math.round(baseRtt * 0.1)); | ||
| return ` | ||
| // ---- performance-timing spoof (PerformanceNavigationTiming) ---------------- | ||
| (function() { | ||
| if (typeof performance === "undefined") return; | ||
| if (typeof performance.getEntriesByType !== "function") return; | ||
| var DNS_MS = ${dnsMs}; | ||
| var TCP_MS = ${tcpMs}; | ||
| var TLS_MS = ${tlsMs}; | ||
| var DEFAULT_PROTOCOL = "h2"; | ||
| function patchEntry(entry) { | ||
| if (entry === null || entry === undefined) return entry; | ||
| if (entry.entryType !== "navigation") return entry; | ||
| return new Proxy(entry, { | ||
| get: function(target, prop, receiver) { | ||
| if (prop === "domainLookupEnd") { | ||
| var s = target.domainLookupStart; | ||
| var e = target.domainLookupEnd; | ||
| return (e <= s) ? s + DNS_MS : e; | ||
| } | ||
| if (prop === "connectEnd") { | ||
| var cs = target.connectStart; | ||
| var ce = target.connectEnd; | ||
| return (ce <= cs) ? cs + TCP_MS + TLS_MS : ce; | ||
| } | ||
| if (prop === "secureConnectionStart") { | ||
| var v = target.secureConnectionStart; | ||
| if (v === 0 || v === undefined) { | ||
| return target.connectStart + TCP_MS; | ||
| } | ||
| return v; | ||
| } | ||
| if (prop === "nextHopProtocol") { | ||
| var p = target.nextHopProtocol; | ||
| return (p === "" || p === undefined) ? DEFAULT_PROTOCOL : p; | ||
| } | ||
| if (prop === "toJSON") { | ||
| // Page scripts that JSON-serialise the entry must see the same | ||
| // patched values rather than the raw zeroes. | ||
| return function() { | ||
| var orig = (typeof target.toJSON === "function") ? target.toJSON() : Object.assign({}, target); | ||
| var ds = target.domainLookupStart; | ||
| if (orig.domainLookupEnd <= ds) orig.domainLookupEnd = ds + DNS_MS; | ||
| var cs = target.connectStart; | ||
| if (orig.connectEnd <= cs) orig.connectEnd = cs + TCP_MS + TLS_MS; | ||
| if (orig.secureConnectionStart === 0 || orig.secureConnectionStart === undefined) { | ||
| orig.secureConnectionStart = cs + TCP_MS; | ||
| } | ||
| if (orig.nextHopProtocol === "" || orig.nextHopProtocol === undefined) { | ||
| orig.nextHopProtocol = DEFAULT_PROTOCOL; | ||
| } | ||
| return orig; | ||
| }; | ||
| } | ||
| return Reflect.get(target, prop, receiver); | ||
| }, | ||
| }); | ||
| } | ||
| var origByType = performance.getEntriesByType; | ||
| function getEntriesByType(type) { | ||
| var r = __mochi_apply__.call(origByType, this, [type]); | ||
| if (type === "navigation" && Array.isArray(r)) { | ||
| return r.map(patchEntry); | ||
| } | ||
| return r; | ||
| } | ||
| __mochi_register_native__(getEntriesByType, "getEntriesByType"); | ||
| var origGetEntries = performance.getEntries; | ||
| function getEntries() { | ||
| var r = __mochi_apply__.call(origGetEntries, this, []); | ||
| if (Array.isArray(r)) { | ||
| return r.map(patchEntry); | ||
| } | ||
| return r; | ||
| } | ||
| __mochi_register_native__(getEntries, "getEntries"); | ||
| var origByName = performance.getEntriesByName; | ||
| function getEntriesByName(name, type) { | ||
| var r = (type === undefined) | ||
| ? __mochi_apply__.call(origByName, this, [name]) | ||
| : __mochi_apply__.call(origByName, this, [name, type]); | ||
| if (Array.isArray(r)) { | ||
| return r.map(patchEntry); | ||
| } | ||
| return r; | ||
| } | ||
| __mochi_register_native__(getEntriesByName, "getEntriesByName"); | ||
| try { | ||
| __mochi_defineProperty__(performance, "getEntriesByType", { | ||
| configurable: true, enumerable: false, writable: true, value: getEntriesByType, | ||
| }); | ||
| __mochi_defineProperty__(performance, "getEntries", { | ||
| configurable: true, enumerable: false, writable: true, value: getEntries, | ||
| }); | ||
| __mochi_defineProperty__(performance, "getEntriesByName", { | ||
| configurable: true, enumerable: false, writable: true, value: getEntriesByName, | ||
| }); | ||
| } catch (_e) {} | ||
| })(); | ||
| `; | ||
| } |
+1
-1
| { | ||
| "name": "@mochi.js/inject", | ||
| "version": "0.3.1", | ||
| "version": "0.4.0", | ||
| "description": "Zero-jitter stealth payload for mochi — JIT-friendly proxies installed before any page script.", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
+2
-0
@@ -38,2 +38,3 @@ /** | ||
| import { emitNetworkInfoModule } from "./modules/network-info"; | ||
| import { emitPerformanceTimingModule } from "./modules/performance-timing"; | ||
| import { emitPermissionsModule } from "./modules/permissions"; | ||
@@ -113,2 +114,3 @@ import { emitPluginsModule } from "./modules/plugins"; | ||
| parts.push(wrapTry("network-info", emitNetworkInfoModule(matrix))); | ||
| parts.push(wrapTry("performance-timing", emitPerformanceTimingModule(matrix))); | ||
| parts.push(wrapTry("permissions", emitPermissionsModule(matrix))); | ||
@@ -115,0 +117,0 @@ parts.push(wrapTry("screen-orientation", emitScreenOrientationModule(matrix))); |
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
190403
6.3%35
6.06%4429
6.08%16
6.67%