From 359527569f1901cf44ee36a1b5f61252e26b5673 Mon Sep 17 00:00:00 2001 From: PJ Date: Sun, 3 May 2026 11:05:47 +0700 Subject: [PATCH] fix(web-runtime): lock global runtime hooks against page shadowing AddScriptToEvaluateOnNewDocument runs first, but a page script can still delete or replace window.__sanderling{,Extractors__,NextAction__} between install and host invocation. Define them as non-writable, non-configurable properties so any attempt to shadow them throws in strict mode rather than silently breaking the run. --- pkg/spec/src/web-runtime.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/pkg/spec/src/web-runtime.ts b/pkg/spec/src/web-runtime.ts index 347234f..6e52b0c 100644 --- a/pkg/spec/src/web-runtime.ts +++ b/pkg/spec/src/web-runtime.ts @@ -347,7 +347,19 @@ const runtime = { pressKeys: { __sanderlingActionGenerator: true, __sanderlingKind: "pressKey" } as ActionGeneratorHandle, }; -(globalThis as Record).__sanderling__ = runtime; +// Lock the runtime globals so a misbehaving (or malicious) page script can't +// shadow or replace them between AddScriptToEvaluateOnNewDocument running and +// the host invoking the extractor/next-action callbacks. +defineLockedGlobal("__sanderling__", runtime); + +function defineLockedGlobal(name: string, value: unknown): void { + Object.defineProperty(globalThis, name, { + value, + writable: false, + configurable: false, + enumerable: false, + }); +} function evaluateExtractors(): Record { const state = buildState(); @@ -524,11 +536,11 @@ function pointOf(value: unknown): { x: number; y: number } | undefined { return undefined; } -(globalThis as Record).__sanderlingExtractors__ = function (): Record { +defineLockedGlobal("__sanderlingExtractors__", function (): Record { return evaluateExtractors(); -}; +}); -(globalThis as Record).__sanderlingNextAction__ = function (): unknown { +defineLockedGlobal("__sanderlingNextAction__", function (): unknown { if (!actionsRoot) return null; // Match the goja runtime: retry up to 16 times when a weighted entry's // generator returns []. Otherwise on routes where most generators are @@ -538,6 +550,6 @@ function pointOf(value: unknown): { x: number; y: number } | undefined { if (action !== null) return action; } return null; -}; +}); export {};