|
20 | 20 | const documentCookieRecords = []; |
21 | 21 | initDocumentCookieRecords(documentCookie); |
22 | 22 | const urlMeta = new WeakMap(); |
23 | | - const iframeMeta = new WeakSet(); |
| 23 | + const networkContainmentMarker = Symbol.for('zeroproxy.network.contained'); |
| 24 | + const iframeHooksMarker = Symbol.for('zeroproxy.iframe.hooks'); |
24 | 25 | const listenersKey = Symbol('zp.listeners'); |
25 | 26 | const workerBlobURLs = new Set(); |
26 | 27 |
|
|
365 | 366 | try { |
366 | 367 | new MO(records => { |
367 | 368 | for (const r of records) { |
368 | | - if (r.type === 'attributes') syncBaseElement(r.target); |
369 | | - else for (const n of r.addedNodes || []) syncBaseElement(n); |
| 369 | + if (r.type === 'attributes') enforceObservedAttribute(r.target, String(r.attributeName || '').toLowerCase()); |
| 370 | + else for (const n of r.addedNodes || []) { syncBaseElement(n); instrumentDescendantIframes(n); } |
370 | 371 | } |
371 | | - }).observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['href'] }); |
| 372 | + }).observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['href', 'src', 'srcdoc', 'action', 'formaction'] }); |
372 | 373 | } catch {} |
373 | 374 | } |
| 375 | + function enforceObservedAttribute(el, key) { |
| 376 | + if (!el || !key) return; |
| 377 | + const tag = el.localName; |
| 378 | + if (tag === 'base' && key === 'href') { syncBaseElement(el); return; } |
| 379 | + if ((tag === 'iframe' || tag === 'frame') && key === 'srcdoc') { |
| 380 | + const raw = Native.getAttribute.call(el, 'srcdoc'); |
| 381 | + if (raw && !raw.startsWith(injectSrcdoc(''))) Native.setAttribute.call(el, 'srcdoc', injectSrcdoc(String(raw))); |
| 382 | + instrumentIframe(el); |
| 383 | + return; |
| 384 | + } |
| 385 | + if (!isURLBearing(el, key)) return; |
| 386 | + const raw = Native.getAttribute.call(el, key); |
| 387 | + if (!raw || !isHTTPURL(raw) || String(raw).startsWith(proxyOrigin)) return; |
| 388 | + let target; |
| 389 | + try { target = targetURL(raw); } catch { return; } |
| 390 | + const alreadyMapped = urlMeta.get(el) === target && Native.getAttribute.call(el, 'data-zp-target-url') === target; |
| 391 | + urlMeta.set(el, target); |
| 392 | + Native.setAttribute.call(el, 'data-zp-target-url', target); |
| 393 | + if ((tag === 'iframe' || tag === 'frame') && key === 'src') { |
| 394 | + Native.setAttribute.call(el, key, 'about:blank'); |
| 395 | + shareNavURL(target).then(u => Native.setAttribute.call(el, key, u)).catch(()=>{}); |
| 396 | + instrumentIframe(el); |
| 397 | + return; |
| 398 | + } |
| 399 | + if (alreadyMapped) return; |
| 400 | + Native.setAttribute.call(el, key, target); |
| 401 | + } |
374 | 402 |
|
375 | 403 | function installWorkerHooks() { |
376 | 404 | if (Native.Worker) define(root, 'Worker', function(url, opts) { return new Native.Worker(workerBootstrapURL(url), opts); }); |
|
402 | 430 | } |
403 | 431 |
|
404 | 432 | function installIframeHooks(w) { |
405 | | - define(w.document, 'createElement', function(name, opts) { const el = Native.createElement(String(name), opts); if (/^i?frame$/i.test(String(name))) queueMicrotask(() => instrumentIframe(el)); return el; }); |
406 | | - for (const [proto, name, nativeFn] of [[w.Node.prototype,'appendChild',Native.appendChild],[w.Node.prototype,'insertBefore',Native.insertBefore],[w.Node.prototype,'replaceChild',Native.replaceChild]]) { |
407 | | - define(proto, name, function(...args) { const ret = nativeFn.apply(this, args); for (const a of args) instrumentDescendantIframes(a); return ret; }); |
408 | | - } |
| 433 | + if (!w || !w.document || !w.Node || !w.Element) return; |
| 434 | + try { |
| 435 | + if (w[iframeHooksMarker]) return; |
| 436 | + Object.defineProperty(w, iframeHooksMarker, { value: true, enumerable: false, configurable: false }); |
| 437 | + } catch {} |
| 438 | + const instrumentedWindows = new WeakSet(); |
| 439 | + const nativeCreateElement = w === root ? Native.createElement : w.document.createElement.bind(w.document); |
| 440 | + |
| 441 | + installFrameAccessors(w.HTMLIFrameElement && w.HTMLIFrameElement.prototype); |
| 442 | + installFrameAccessors(w.HTMLFrameElement && w.HTMLFrameElement.prototype); |
| 443 | + |
| 444 | + define(w.document, 'createElement', function(name, opts) { |
| 445 | + const el = nativeCreateElement(String(name), opts); |
| 446 | + if (/^i?frame$/i.test(String(name))) instrumentDescendantIframes(el); |
| 447 | + return el; |
| 448 | + }); |
| 449 | + |
| 450 | + patchInsertion(w.Node.prototype, 'appendChild', w.Node.prototype.appendChild); |
| 451 | + patchInsertion(w.Node.prototype, 'insertBefore', w.Node.prototype.insertBefore); |
| 452 | + patchInsertion(w.Node.prototype, 'replaceChild', w.Node.prototype.replaceChild); |
| 453 | + for (const method of ['append', 'prepend', 'before', 'after', 'replaceWith']) patchInsertion(w.Element.prototype, method, w.Element.prototype[method]); |
| 454 | + |
409 | 455 | if (w.HTMLIFrameElement) { installFrameProp(w.HTMLIFrameElement.prototype, 'src'); installFrameProp(w.HTMLIFrameElement.prototype, 'srcdoc'); } |
410 | | - function installFrameProp(proto, prop) { const d = Object.getOwnPropertyDescriptor(proto, prop); if (!d || !d.set) return; try { Object.defineProperty(proto, prop, { get: d.get, set(v) { if (prop === 'srcdoc') d.set.call(this, injectSrcdoc(String(v))); else if (isHTTPURL(v)) { d.set.call(this, 'about:blank'); shareNavURL(v).then(u => d.set.call(this, u)).catch(()=>{}); } else d.set.call(this, v); instrumentIframe(this); }, configurable: false }); } catch {} } |
| 456 | + if (w.HTMLFrameElement) installFrameProp(w.HTMLFrameElement.prototype, 'src'); |
| 457 | + |
| 458 | + function patchInsertion(proto, name, nativeFn) { |
| 459 | + if (!proto || typeof nativeFn !== 'function') return; |
| 460 | + define(proto, name, function(...args) { |
| 461 | + const frames = collectIframesFromArgs(args); |
| 462 | + const ret = nativeFn.apply(this, args); |
| 463 | + instrumentFrameList(frames); |
| 464 | + return ret; |
| 465 | + }); |
| 466 | + } |
| 467 | + function installFrameAccessors(proto) { |
| 468 | + if (!proto) return; |
| 469 | + const win = frameDescriptor(proto, 'contentWindow'); |
| 470 | + if (win && win.get) { |
| 471 | + try { Object.defineProperty(proto, 'contentWindow', { get() { return containFrameWindow(win.get.call(this), this); }, configurable: false, enumerable: true }); } catch {} |
| 472 | + } |
| 473 | + const doc = frameDescriptor(proto, 'contentDocument'); |
| 474 | + if (doc && doc.get) { |
| 475 | + try { Object.defineProperty(proto, 'contentDocument', { get() { const childDoc = doc.get.call(this); if (childDoc && childDoc.defaultView) containFrameWindow(childDoc.defaultView, this); return childDoc; }, configurable: false, enumerable: true }); } catch {} |
| 476 | + } |
| 477 | + } |
| 478 | + function frameDescriptor(proto, prop) { |
| 479 | + for (let p = proto; p; p = Object.getPrototypeOf(p)) { |
| 480 | + const d = Object.getOwnPropertyDescriptor(p, prop); |
| 481 | + if (d) return d; |
| 482 | + } |
| 483 | + return null; |
| 484 | + } |
| 485 | + function containFrameWindow(childWin, frame) { |
| 486 | + if (!childWin) return childWin; |
| 487 | + try { if (childWin[networkContainmentMarker]) return childWin; } catch { if (instrumentedWindows.has(childWin)) return childWin; } |
| 488 | + instrumentedWindows.add(childWin); |
| 489 | + try { installNetworkContainment(childWin); } |
| 490 | + catch (e) { |
| 491 | + instrumentedWindows.delete(childWin); |
| 492 | + try { frame && frame.remove && frame.remove(); } catch {} |
| 493 | + throw e; |
| 494 | + } |
| 495 | + return childWin; |
| 496 | + } |
| 497 | + function installFrameProp(proto, prop) { |
| 498 | + const d = Object.getOwnPropertyDescriptor(proto, prop); |
| 499 | + if (!d || !d.set) return; |
| 500 | + try { |
| 501 | + Object.defineProperty(proto, prop, { |
| 502 | + get: d.get, |
| 503 | + set(v) { |
| 504 | + if (prop === 'srcdoc') d.set.call(this, injectSrcdoc(String(v))); |
| 505 | + else if (isHTTPURL(v) && !String(v).startsWith(proxyOrigin)) { |
| 506 | + d.set.call(this, 'about:blank'); |
| 507 | + shareNavURL(v).then(u => d.set.call(this, u)).catch(()=>{}); |
| 508 | + } else d.set.call(this, v); |
| 509 | + instrumentIframe(this); |
| 510 | + }, |
| 511 | + configurable: false |
| 512 | + }); |
| 513 | + } catch {} |
| 514 | + } |
| 515 | + } |
| 516 | + function collectIframesFromArgs(args) { |
| 517 | + let frames = null; |
| 518 | + for (const node of args) frames = collectIframes(node, frames); |
| 519 | + return frames; |
| 520 | + } |
| 521 | + function collectIframes(node, frames) { |
| 522 | + if (!node || typeof node !== 'object') return frames; |
| 523 | + if (/^(IFRAME|FRAME)$/.test(node.nodeName || '')) { |
| 524 | + if (!frames) frames = []; |
| 525 | + frames.push(node); |
| 526 | + } |
| 527 | + if (node.querySelectorAll) { |
| 528 | + const descendants = node.querySelectorAll('iframe,frame'); |
| 529 | + for (let i = 0; i < descendants.length; i++) { |
| 530 | + if (!frames) frames = []; |
| 531 | + frames.push(descendants[i]); |
| 532 | + } |
| 533 | + } |
| 534 | + return frames; |
| 535 | + } |
| 536 | + function instrumentFrameList(frames) { if (frames) for (const frame of frames) instrumentIframe(frame); } |
| 537 | + function instrumentDescendantIframes(node) { instrumentFrameList(collectIframes(node, null)); } |
| 538 | + function instrumentIframe(frame) { |
| 539 | + if (!frame || !/^(IFRAME|FRAME)$/.test(frame.nodeName || '')) return; |
| 540 | + try { |
| 541 | + const src = Native.getAttribute.call(frame, 'src'); |
| 542 | + if ((!src || /^about:blank$/i.test(src)) && frame.contentWindow) installNetworkContainment(frame.contentWindow); |
| 543 | + } catch { try { frame.remove(); } catch {} } |
411 | 544 | } |
412 | | - function instrumentDescendantIframes(node) { if (!node || !node.querySelectorAll) { if (node && /^(IFRAME|FRAME)$/.test(node.nodeName)) instrumentIframe(node); return; } node.querySelectorAll('iframe,frame').forEach(instrumentIframe); } |
413 | | - function instrumentIframe(frame) { if (!frame || iframeMeta.has(frame)) return; iframeMeta.add(frame); try { if (!frame.getAttribute('src') && frame.contentWindow) installNetworkContainment(frame.contentWindow); } catch { try { frame.remove(); } catch {} } } |
414 | 545 | function installNetworkContainment(w) { |
| 546 | + if (!w) return; |
| 547 | + try { if (w[networkContainmentMarker]) return; } catch {} |
415 | 548 | // Native fetch is intentionally left intact; the Service Worker owns request capture. |
416 | 549 | installNavigatorIdentity(w); |
417 | | - if (root.WebSocket) define(w, 'WebSocket', root.WebSocket); |
| 550 | + if (root.WebSocket && !define(w, 'WebSocket', root.WebSocket)) throw normalizedError('SecurityError'); |
418 | 551 | if (w.navigator && navigator.sendBeacon) define(w.navigator, 'sendBeacon', navigator.sendBeacon.bind(navigator)); |
419 | | - installBlockers(w); |
| 552 | + installIframeHooks(w); |
| 553 | + installBlockers(w, true); |
| 554 | + try { Object.defineProperty(w, networkContainmentMarker, { value: true, enumerable: false, configurable: false }); } catch {} |
420 | 555 | } |
421 | 556 |
|
422 | | - function installBlockers(w) { |
| 557 | + function installBlockers(w, strict = false) { |
423 | 558 | const blockCtor = function(){ throw normalizedError('NotSupportedError'); }; |
424 | | - for (const name of ['RTCPeerConnection','webkitRTCPeerConnection','RTCDataChannel','WebTransport','WebSocketStream']) define(w, name, blockCtor); |
| 559 | + for (const name of ['RTCPeerConnection','webkitRTCPeerConnection','RTCDataChannel','WebTransport','WebSocketStream']) { |
| 560 | + const ok = define(w, name, blockCtor); |
| 561 | + if (strict && name in w && !ok) throw normalizedError('SecurityError'); |
| 562 | + } |
425 | 563 | const nav = w.navigator; |
426 | 564 | if (nav) { |
427 | 565 | for (const name of ['serial','hid','usb','bluetooth','requestMIDIAccess','credentials','geolocation','clipboard','wakeLock']) { try { Object.defineProperty(nav, name, { get(){ throw normalizedError('NotSupportedError'); }, configurable: false }); } catch {} } |
|
0 commit comments