Skip to content

Commit 7b5c89f

Browse files
committed
feat: support initial about:blank frames by introducing defineReplacingAccessor and improving document URL/origin virtualization.
1 parent 79616c2 commit 7b5c89f

4 files changed

Lines changed: 56 additions & 46 deletions

File tree

test/e2e/expected-deltas.json

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,6 @@
2525
"pattern": "^surface\\.fingerprint\\.canvas\\.(length|stableRead)$",
2626
"reason": "Canvas export randomization intentionally changes repeated data URL reads."
2727
},
28-
{
29-
"id": "window-screen-position-persona",
30-
"pattern": "^surface\\.fingerprint\\.objectPropertyCollection\\.r\\.(0|22|[1-9][0-9]*)$",
31-
"reason": "The proxy applies the Windows Chrome persona and normalizes screen/window coordinates."
32-
},
3328
{
3429
"id": "navigator-app-version-persona",
3530
"pattern": "^surface\\.fingerprint\\.objectPropertyCollection\\.r\\.5\\.0 \\((Macintosh; Intel Mac OS X 10_15_7|Windows NT 10\\.0; Win64; x64)\\) AppleWebKit/537\\.36 \\(KHTML, like Gecko\\) (HeadlessChrome|Chrome)/[0-9]+\\.0\\.0\\.0 Safari/537\\.36$",
@@ -44,36 +39,6 @@
4439
"id": "navigator-platform-persona",
4540
"pattern": "^surface\\.fingerprint\\.objectPropertyCollection\\.r\\.(MacIntel|Win32)$",
4641
"reason": "Native Chromium exposes host platform while ZeroProxy exposes the Windows Chrome persona."
47-
},
48-
{
49-
"id": "initial-blank-document-url",
50-
"pattern": "^surface\\.fingerprint\\.objectPropertyCollection\\.r\\.about:blank$",
51-
"reason": "The native clean iframe document starts at about:blank; ZeroProxy maps contained frame document URLs through virtual location facades."
52-
},
53-
{
54-
"id": "target-origin-bucket",
55-
"pattern": "^surface\\.fingerprint\\.objectPropertyCollection\\.r\\.http://localhost:[0-9]+$",
56-
"reason": "Native exposes the target test origin while ZeroProxy exposes the proxy origin."
57-
},
58-
{
59-
"id": "target-document-url-bucket",
60-
"pattern": "^surface\\.fingerprint\\.objectPropertyCollection\\.r\\.http://localhost:[0-9]+/differential-fixture$",
61-
"reason": "Native exposes the target fixture URL while ZeroProxy virtualizes frame document URL values."
62-
},
63-
{
64-
"id": "document-domain-bucket",
65-
"pattern": "^surface\\.fingerprint\\.objectPropertyCollection\\.r\\.(localhost|proxy\\.localhost)$",
66-
"reason": "Native document.domain is the target host while ZeroProxy runs on the proxy host."
67-
},
68-
{
69-
"id": "opaque-proxy-origin-bucket",
70-
"pattern": "^surface\\.fingerprint\\.objectPropertyCollection\\.r\\.null$",
71-
"reason": "Contained ZeroProxy frame origin can be opaque for protected about:blank/srcdoc boundary cases."
72-
},
73-
{
74-
"id": "document-cookie-presence",
75-
"pattern": "^surface\\.fingerprint\\.objectPropertyCollection\\.r\\.s$",
76-
"reason": "The raw oracle records document.cookie presence/type only; normalized comparison ignores cookie value exposure."
7742
}
7843
]
7944
}

test/e2e/proxy.test.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -419,13 +419,17 @@ function createTargetServer(requests) {
419419
// ============================================================================
420420
// 4. MAIN EXECUTION CONTROLLER (SANDBOX ISOLATION)
421421
// ============================================================================
422-
const getFingerPrint = () => {
422+
const waitForFrameLayout = () => new Promise((resolve) => {
423+
requestAnimationFrame(() => requestAnimationFrame(resolve));
424+
});
425+
const getFingerPrint = async () => {
423426
const doc = window.document;
424427
try {
425428
const iframe = doc.createElement('iframe');
426429
iframe.style.display = 'none';
427430
iframe.tabIndex = '-1';
428431
doc.body.appendChild(iframe);
432+
await waitForFrameLayout();
429433
const iframeWin = iframe.contentWindow;
430434
let dataStore = {};
431435
dataStore = buildObjectSnapshot(iframeWin, iframeWin, '', dataStore);
@@ -443,8 +447,8 @@ function createTargetServer(requests) {
443447
};
444448
}
445449
};
446-
const objectPropertyCollectionFingerprint = () => {
447-
const result = getFingerPrint();
450+
const objectPropertyCollectionFingerprint = async () => {
451+
const result = await getFingerPrint();
448452
if (result.e === null) {
449453
sortFingerprintRecords(result.r);
450454
let jsonString = JSON.stringify(result.r);
@@ -682,7 +686,7 @@ function createTargetServer(requests) {
682686
frame: frameObservations(),
683687
fingerprint: {
684688
...fingerprintSurfaceObservations(),
685-
objectPropertyCollection: objectPropertyCollectionFingerprint()
689+
objectPropertyCollection: await objectPropertyCollectionFingerprint()
686690
}
687691
}
688692
};

web/runtime-prelude.mjs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ import { createWorkerFacades } from './runtime/workers/facades.mjs';
9191
const normalizedError = createNormalizedError(Native);
9292
const {
9393
nativeFunctionSource,
94+
nativeAccessorSource,
9495
maskNativeFunction,
9596
maskMethods,
9697
define,
@@ -112,6 +113,23 @@ import { createWorkerFacades } from './runtime/workers/facades.mjs';
112113
return define(obj, key, value);
113114
}
114115
}
116+
function defineReplacingAccessor(obj, key, get, set) {
117+
if (!obj) return false;
118+
const d = Object.getOwnPropertyDescriptor(obj, key);
119+
try {
120+
Object.defineProperty(obj, key, {
121+
get,
122+
set,
123+
enumerable: d ? d.enumerable : true,
124+
configurable: d ? d.configurable : true
125+
});
126+
if (typeof get === 'function') toStringMap.set(get, nativeAccessorSource('get', key));
127+
if (typeof set === 'function') toStringMap.set(set, nativeAccessorSource('set', key));
128+
return true;
129+
} catch {
130+
return defineAccessor(obj, key, get, set);
131+
}
132+
}
115133
const {
116134
installCanvasAntiFingerprinting,
117135
installAudioAntiFingerprinting,
@@ -136,6 +154,7 @@ import { createWorkerFacades } from './runtime/workers/facades.mjs';
136154
boot,
137155
Native,
138156
defineAccessor,
157+
defineReplacingAccessor,
139158
getVirtualURL: () => virtualURL,
140159
getBaseURL: () => baseURL,
141160
postMessageToSW,
@@ -660,6 +679,18 @@ import { createWorkerFacades } from './runtime/workers/facades.mjs';
660679
catch { return false; }
661680
}
662681

682+
function isInitialAboutBlankFrame(frame, childWin) {
683+
if (!frame || isSrcdocFrame(frame)) return false;
684+
try {
685+
if (Native.getAttribute.call(frame, 'data-zp-target-url') || Native.getAttribute.call(frame, 'data-zp-blocked-url')) return false;
686+
const rawSrc = String(Native.getAttribute.call(frame, 'src') || '').trim().toLowerCase();
687+
if (rawSrc && rawSrc !== 'about:blank') return false;
688+
return true;
689+
} catch {
690+
return false;
691+
}
692+
}
693+
663694
function frameLocationFacadeFor(frame, childDoc, childWin) {
664695
const current = () => {
665696
try { return new URL(frameDocumentURL(frame, childDoc, childWin)); }
@@ -695,7 +726,7 @@ import { createWorkerFacades } from './runtime/workers/facades.mjs';
695726
if (prop === Symbol.toStringTag) return 'Window';
696727
if (prop === 'window' || prop === 'self' || prop === 'globalThis' || prop === 'frames') return proxy;
697728
if (prop === 'location') return locationFacade;
698-
if (prop === 'origin') return locationFacade.origin;
729+
if (prop === 'origin') return isInitialAboutBlankFrame(frame, childWin) ? virtualURL.origin : locationFacade.origin;
699730
if (prop === 'postMessage') return postMessageWrapperFor(childWin);
700731
if (prop === 'document') {
701732
try { return frameDocumentFacadeFor(frame, childWin.document, childWin); } catch { return undefined; }
@@ -778,6 +809,14 @@ import { createWorkerFacades } from './runtime/workers/facades.mjs';
778809
if (prop === Symbol.toStringTag) return 'HTMLDocument';
779810
if (prop === 'defaultView') return windowFacade;
780811
if (prop === 'location') return locationFacade;
812+
if (isInitialAboutBlankFrame(frame, childWin)) {
813+
if (prop === 'URL' || prop === 'documentURI') return 'about:blank';
814+
if (prop === 'referrer') return virtualURL.href;
815+
if (prop === 'domain') return virtualURL.hostname;
816+
if (prop === 'cookie') {
817+
try { return document.cookie; } catch { return ''; }
818+
}
819+
}
781820
if (prop === 'URL' || prop === 'documentURI') return locationFacade.href;
782821
const value = childDoc[prop];
783822
return typeof value === 'function' ? value.bind(childDoc) : value;
@@ -1164,6 +1203,7 @@ import { createWorkerFacades } from './runtime/workers/facades.mjs';
11641203
installDynamicCodeHooks();
11651204
installDocumentWriteHooks(root);
11661205
}
1206+
11671207
function fireEvent(target, type) {
11681208
let ev;
11691209
try { ev = new Event(type); } catch { ev = { type }; }

web/runtime/facades/document.mjs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export function createDocumentFacades({
33
boot,
44
Native,
55
defineAccessor,
6+
defineReplacingAccessor = defineAccessor,
67
getVirtualURL,
78
getBaseURL,
89
postMessageToSW,
@@ -11,11 +12,11 @@ export function createDocumentFacades({
1112
initDocumentCookieRecords(String(boot.documentCookie || ''));
1213

1314
function installDocumentAccessors(w) {
14-
defineAccessor(w.Document && w.Document.prototype, 'URL', () => getVirtualURL().href);
15-
defineAccessor(w.Document && w.Document.prototype, 'documentURI', () => getVirtualURL().href);
16-
defineAccessor(w.Document && w.Document.prototype, 'baseURI', () => getBaseURL());
17-
defineAccessor(w.Document && w.Document.prototype, 'referrer', () => boot.documentReferrer || '');
18-
defineAccessor(w.Document && w.Document.prototype, 'cookie', () => documentCookieString(), value => {
15+
defineReplacingAccessor(w.Document && w.Document.prototype, 'URL', () => getVirtualURL().href);
16+
defineReplacingAccessor(w.Document && w.Document.prototype, 'documentURI', () => getVirtualURL().href);
17+
defineReplacingAccessor(w.Document && w.Document.prototype, 'baseURI', () => getBaseURL());
18+
defineReplacingAccessor(w.Document && w.Document.prototype, 'referrer', () => boot.documentReferrer || '');
19+
defineReplacingAccessor(w.Document && w.Document.prototype, 'cookie', () => documentCookieString(), value => {
1920
const cookie = String(value);
2021
setDocumentCookie(cookie);
2122
postMessageToSW({
@@ -25,7 +26,7 @@ export function createDocumentFacades({
2526
cookie
2627
}).catch(()=>{});
2728
});
28-
defineAccessor(w, 'origin', () => {
29+
defineReplacingAccessor(w, 'origin', () => {
2930
try {
3031
return new URL(w.document.URL).origin;
3132
} catch {

0 commit comments

Comments
 (0)