From 02358024f7d493bc9cb0aa5eb3b9bb1dfa785115 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Wed, 9 Sep 2026 22:10:50 +0900 Subject: [PATCH 1/6] fix security --- rspack.config.ts | 2 ++ src/app/service/content/exec_script.ts | 4 +++- src/app/service/content/types.ts | 2 +- src/app/service/content/utils.test.ts | 29 +++++++++++++++++++------- src/app/service/content/utils.ts | 16 +++++++++++--- vitest.config.ts | 2 ++ 6 files changed, 42 insertions(+), 13 deletions(-) diff --git a/rspack.config.ts b/rspack.config.ts index f9e8ae578..35d02137b 100644 --- a/rspack.config.ts +++ b/rspack.config.ts @@ -138,6 +138,8 @@ export default { new rspack.DefinePlugin({ "process.env.VI_TESTING": "'false'", "process.env.SC_RANDOM_KEY": `'${uuidv4()}'`, + "process.env.SC_RANDOM_FNKEY": `'${uuidv4()}'`, + "process.env.SC_ZN_RAND": `'$${uuidv4()}'`, "process.env.SC_DISABLE_AGENT": `'${enableAgent ? "false" : "true"}'`, }), new rspack.CopyRspackPlugin({ diff --git a/src/app/service/content/exec_script.ts b/src/app/service/content/exec_script.ts index 6c5b93730..7f30bf4e7 100644 --- a/src/app/service/content/exec_script.ts +++ b/src/app/service/content/exec_script.ts @@ -9,6 +9,8 @@ import { evaluateGMInfo } from "./gm_api/gm_info"; import type { IGM_Base } from "./gm_api/gm_api"; import type { TScriptInfo } from "@App/app/repo/scripts"; +const fnStrIntegrity = process.env.SC_RANDOM_FNKEY!; + // 执行脚本,控制脚本执行与停止 export default class ExecScript { scriptRes: TScriptInfo; @@ -88,7 +90,7 @@ export default class ExecScript { this.logger.debug("script start"); const sandboxContext = this.sandboxContext; this.execContext = sandboxContext ? createProxyContext(sandboxContext) : global; // this.$ 只能执行一次 - return this.scriptFunc.call(this.execContext, this.named, this.scriptRes.name); + return this.scriptFunc(fnStrIntegrity, this.execContext, this.named, this.scriptRes.name); }; // 早期启动的脚本,处理GM API diff --git a/src/app/service/content/types.ts b/src/app/service/content/types.ts index 30fe88e80..61d0f060f 100644 --- a/src/app/service/content/types.ts +++ b/src/app/service/content/types.ts @@ -1,6 +1,6 @@ import type { REncoded } from "@App/pkg/utils/message_value"; -export type ScriptFunc = (named: { [key: string]: any } | undefined, scriptName: string) => any; +export type ScriptFunc = (s: string, ctx: any, named: { [key: string]: any } | undefined, scriptName: string) => any; // exec_script.ts diff --git a/src/app/service/content/utils.test.ts b/src/app/service/content/utils.test.ts index 468ef94fe..6f9579ba6 100644 --- a/src/app/service/content/utils.test.ts +++ b/src/app/service/content/utils.test.ts @@ -12,6 +12,9 @@ import type { ScriptRunResource } from "@App/app/repo/scripts"; import type { ScriptFunc } from "./types"; import { RuleType, type URLRuleEntry } from "@App/pkg/utils/url_matcher"; +const fnStrIntegrity = process.env.SC_RANDOM_FNKEY!; +const znRand = process.env.SC_ZN_RAND!; + // 设置 console mock 来避免测试输出污染 vi.spyOn(console, "error").mockImplementation(() => {}); vi.spyOn(console, "log").mockImplementation(() => {}); @@ -249,7 +252,7 @@ describe("utils", () => { const code = "return arguments[0].value + arguments[1];"; const func: ScriptFunc = compileScript(code); - const result = func({ value: 10 }, "test-script"); + const result = func(fnStrIntegrity, {}, { value: 10 }, "test-script"); expect(result).toBe("10test-script"); }); @@ -265,8 +268,8 @@ describe("utils", () => { `; const func: ScriptFunc = compileScript(code); - const result1 = func({ value: 5, multiply: 3 }, "test"); - const result2 = func({ value: 5 }, "fallback"); + const result1 = func(fnStrIntegrity, {}, { value: 5, multiply: 3 }, "test"); + const result2 = func(fnStrIntegrity, {}, { value: 5 }, "fallback"); expect(result1).toBe(15); expect(result2).toBe("fallback"); @@ -280,7 +283,7 @@ describe("utils", () => { `; const func: ScriptFunc = compileScript(code); - const result = await func({ value: 5 }, "async-test"); + const result = await func(fnStrIntegrity, {}, { value: 5 }, "async-test"); expect(result).toBe(10); }); @@ -289,7 +292,13 @@ describe("utils", () => { const code = "throw new Error('Test error');"; const func: ScriptFunc = compileScript(code); - expect(() => func({}, "error-test")).toThrow("Test error"); + expect(() => func(fnStrIntegrity, {}, {}, "error-test")).toThrow("Test error"); + }); + + it.concurrent("完整性标记不匹配时不应执行脚本", () => { + const func: ScriptFunc = compileScript("throw new Error('should not run');"); + + expect(func("invalid", {}, {}, "blocked")).toBeUndefined(); }); }); @@ -319,7 +328,9 @@ describe("utils", () => { const result = compileInjectScript(script, scriptCode); - expect(result).toBe(`window['inject-test-flag'] = function(){console.log('injected');}`); + expect(result).toBe( + `window['inject-test-flag'] = (function (k, fn, t, u, ...args) { if (t === k) { t = '${znRand}'; u[t] = fn; return u[t](...args, (u[t] = undefined)) } }).bind(null, '${fnStrIntegrity}', function(){console.log('injected');});` + ); }); it.concurrent("应该包含自动删除挂载函数的代码", () => { @@ -331,7 +342,7 @@ describe("utils", () => { expect(result).toContain(`try{delete window['inject-test-flag']}catch(e){}`); expect(result).toContain("console.log('with auto delete');"); expect(result).toBe( - `window['inject-test-flag'] = function(){try{delete window['inject-test-flag']}catch(e){}console.log('with auto delete');}` + `window['inject-test-flag'] = (function (k, fn, t, u, ...args) { if (t === k) { t = '${znRand}'; u[t] = fn; return u[t](...args, (u[t] = undefined)) } }).bind(null, '${fnStrIntegrity}', function(){try{delete window['inject-test-flag']}catch(e){}console.log('with auto delete');});` ); }); @@ -342,7 +353,9 @@ describe("utils", () => { const result = compileInjectScript(script, scriptCode); expect(result).not.toContain("try{delete window"); - expect(result).toBe(`window['inject-test-flag'] = function(){console.log('without auto delete');}`); + expect(result).toBe( + `window['inject-test-flag'] = (function (k, fn, t, u, ...args) { if (t === k) { t = '${znRand}'; u[t] = fn; return u[t](...args, (u[t] = undefined)) } }).bind(null, '${fnStrIntegrity}', function(){console.log('without auto delete');});` + ); }); it.concurrent("应该处理复杂的脚本代码", () => { diff --git a/src/app/service/content/utils.ts b/src/app/service/content/utils.ts index 8f464773d..5f3ce9669 100644 --- a/src/app/service/content/utils.ts +++ b/src/app/service/content/utils.ts @@ -6,6 +6,9 @@ import { sourceMapTo } from "@App/pkg/utils/utils"; import { ScriptEnvTag } from "@Packages/message/consts"; import { embeddedPatternCheckerString, type EmbeddedURLRuleEntry, type URLRuleEntry } from "@App/pkg/utils/url_matcher"; +const lnStrIntegrity = process.env.SC_RANDOM_FNKEY; +const znRand = process.env.SC_ZN_RAND; + export type CompileScriptCodeResource = { name: string; code: string; @@ -160,7 +163,14 @@ export function compileScriptCodeByResource(resource: CompileScriptCodeResource) // 通过脚本代码编译脚本函数 export function compileScript(code: string): ScriptFunc { - return new Function(code); + const fn = new Function(code); + return function (k: any, fn: any, t: any, u: any, ...args: any[]) { + if (t === k) { + t = `${znRand}`; + u[t] = fn; + return u[t](...args, (u[t] = undefined)); + } + }.bind(null, lnStrIntegrity, fn); } /** @@ -183,7 +193,7 @@ export function compileInjectScriptByFlag( autoDeleteMountFunction: boolean = false ): string { const autoDeleteMountCode = autoDeleteMountFunction ? `try{delete window['${flag}']}catch(e){}` : ""; - return `window['${flag}'] = function(){${autoDeleteMountCode}${scriptCode}}`; + return `window['${flag}'] = (function (k, fn, t, u, ...args) { if (t === k) { t = '${znRand}'; u[t] = fn; return u[t](...args, (u[t] = undefined)) } }).bind(null, '${lnStrIntegrity}', function(){${autoDeleteMountCode}${scriptCode}});`; } /** @@ -235,7 +245,7 @@ export function compilePreInjectScript( const autoDeleteMountCode = autoDeleteMountFunction ? `try{delete window['${flag}']}catch(e){}` : ""; const evScriptLoad = `${eventNamePrefix}${DefinedFlags.scriptLoadComplete}`; const evEnvLoad = `${eventNamePrefix}${DefinedFlags.envLoadComplete}`; - return `window['${flag}'] = function(){${autoDeleteMountCode}${scriptCode}}; + return `window['${flag}'] = (function (k, fn, t, u, ...args) { if (t === k) { t = '${znRand}'; u[t] = fn; return u[t](...args, (u[t] = undefined)) } }).bind(null, '${lnStrIntegrity}', function(){${autoDeleteMountCode}${scriptCode}}); { let o = { cancelable: true, detail: { scriptFlag: '${flag}', scriptInfo: (${scriptInfoJSON}) } }, c = typeof cloneInto === "function" ? cloneInto(o, performance) : o, diff --git a/vitest.config.ts b/vitest.config.ts index cb248d264..0ef021b56 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -45,6 +45,8 @@ const sharedTest = { env: { VI_TESTING: "true", SC_RANDOM_KEY: "005a7deb-3a6e-4337-83ea-b9626c02ea38", + SC_RANDOM_FNKEY: "843078d2-403b-4ec0-a6e0-358488e135ec", + SC_ZN_RAND: "4622da29-026c-47d1-a8f8-ee52bad37129", }, }; From d25821d22ccaa24bd8302c1989deac4e1f57aa16 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Thu, 10 Sep 2026 06:29:59 +0900 Subject: [PATCH 2/6] fix security - `.call(this)` --- src/app/service/content/utils.test.ts | 9 +++++---- src/app/service/content/utils.ts | 13 ++++++------- src/app/service/service_worker/utils.test.ts | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/app/service/content/utils.test.ts b/src/app/service/content/utils.test.ts index 6f9579ba6..f176a02f2 100644 --- a/src/app/service/content/utils.test.ts +++ b/src/app/service/content/utils.test.ts @@ -62,7 +62,8 @@ describe("utils", () => { expect(result).toContain("try {"); expect(result).toContain("} catch (e) {"); expect(result).toContain("with(arguments[0]||this.$)"); - expect(result).toContain("return(async function(){"); + expect(result).toContain("this[arguments[0]='$$'+Date.now()/Math.random()]=async function(){"); + expect(result).toContain("return this[arguments[0]](this[arguments[0]]=arguments[0]=void 0);"); }); it.concurrent("应该处理自定义脚本代码参数", () => { @@ -329,7 +330,7 @@ describe("utils", () => { const result = compileInjectScript(script, scriptCode); expect(result).toBe( - `window['inject-test-flag'] = (function (k, fn, t, u, ...args) { if (t === k) { t = '${znRand}'; u[t] = fn; return u[t](...args, (u[t] = undefined)) } }).bind(null, '${fnStrIntegrity}', function(){console.log('injected');});` + `window['inject-test-flag'] = (function (k, fn, t, u, ...args) { if (t === k) { u[t = '${znRand}'] = fn; return u[t](...args, (u[t] = t = undefined)) } }).bind(null, '${fnStrIntegrity}', function(){console.log('injected');});` ); }); @@ -342,7 +343,7 @@ describe("utils", () => { expect(result).toContain(`try{delete window['inject-test-flag']}catch(e){}`); expect(result).toContain("console.log('with auto delete');"); expect(result).toBe( - `window['inject-test-flag'] = (function (k, fn, t, u, ...args) { if (t === k) { t = '${znRand}'; u[t] = fn; return u[t](...args, (u[t] = undefined)) } }).bind(null, '${fnStrIntegrity}', function(){try{delete window['inject-test-flag']}catch(e){}console.log('with auto delete');});` + `window['inject-test-flag'] = (function (k, fn, t, u, ...args) { if (t === k) { u[t = '${znRand}'] = fn; return u[t](...args, (u[t] = t = undefined)) } }).bind(null, '${fnStrIntegrity}', function(){try{delete window['inject-test-flag']}catch(e){}console.log('with auto delete');});` ); }); @@ -354,7 +355,7 @@ describe("utils", () => { expect(result).not.toContain("try{delete window"); expect(result).toBe( - `window['inject-test-flag'] = (function (k, fn, t, u, ...args) { if (t === k) { t = '${znRand}'; u[t] = fn; return u[t](...args, (u[t] = undefined)) } }).bind(null, '${fnStrIntegrity}', function(){console.log('without auto delete');});` + `window['inject-test-flag'] = (function (k, fn, t, u, ...args) { if (t === k) { u[t = '${znRand}'] = fn; return u[t](...args, (u[t] = t = undefined)) } }).bind(null, '${fnStrIntegrity}', function(){console.log('without auto delete');});` ); }); diff --git a/src/app/service/content/utils.ts b/src/app/service/content/utils.ts index 5f3ce9669..803033e21 100644 --- a/src/app/service/content/utils.ts +++ b/src/app/service/content/utils.ts @@ -151,9 +151,9 @@ export function compileScriptCodeByResource(resource: CompileScriptCodeResource) const joinedCode = [ "with(arguments[0]||this.$){", `${preCode}`, - "return(async function(){", + "this[arguments[0]='$$'+Date.now()/Math.random()]=async function(){", `${code}`, - "}).call(this);}", + "};return this[arguments[0]](this[arguments[0]]=arguments[0]=void 0);}", ] .filter(Boolean) .join("\n"); @@ -166,9 +166,8 @@ export function compileScript(code: string): ScriptFunc { const fn = new Function(code); return function (k: any, fn: any, t: any, u: any, ...args: any[]) { if (t === k) { - t = `${znRand}`; - u[t] = fn; - return u[t](...args, (u[t] = undefined)); + u[(t = `${znRand}`)] = fn; + return u[t](...args, (u[t] = t = undefined)); } }.bind(null, lnStrIntegrity, fn); } @@ -193,7 +192,7 @@ export function compileInjectScriptByFlag( autoDeleteMountFunction: boolean = false ): string { const autoDeleteMountCode = autoDeleteMountFunction ? `try{delete window['${flag}']}catch(e){}` : ""; - return `window['${flag}'] = (function (k, fn, t, u, ...args) { if (t === k) { t = '${znRand}'; u[t] = fn; return u[t](...args, (u[t] = undefined)) } }).bind(null, '${lnStrIntegrity}', function(){${autoDeleteMountCode}${scriptCode}});`; + return `window['${flag}'] = (function (k, fn, t, u, ...args) { if (t === k) { u[t = '${znRand}'] = fn; return u[t](...args, (u[t] = t = undefined)) } }).bind(null, '${lnStrIntegrity}', function(){${autoDeleteMountCode}${scriptCode}});`; } /** @@ -245,7 +244,7 @@ export function compilePreInjectScript( const autoDeleteMountCode = autoDeleteMountFunction ? `try{delete window['${flag}']}catch(e){}` : ""; const evScriptLoad = `${eventNamePrefix}${DefinedFlags.scriptLoadComplete}`; const evEnvLoad = `${eventNamePrefix}${DefinedFlags.envLoadComplete}`; - return `window['${flag}'] = (function (k, fn, t, u, ...args) { if (t === k) { t = '${znRand}'; u[t] = fn; return u[t](...args, (u[t] = undefined)) } }).bind(null, '${lnStrIntegrity}', function(){${autoDeleteMountCode}${scriptCode}}); + return `window['${flag}'] = (function (k, fn, t, u, ...args) { if (t === k) { u[t = '${znRand}'] = fn; return u[t](...args, (u[t] = t = undefined)) } }).bind(null, '${lnStrIntegrity}', function(){${autoDeleteMountCode}${scriptCode}}); { let o = { cancelable: true, detail: { scriptFlag: '${flag}', scriptInfo: (${scriptInfoJSON}) } }, c = typeof cloneInto === "function" ? cloneInto(o, performance) : o, diff --git a/src/app/service/service_worker/utils.test.ts b/src/app/service/service_worker/utils.test.ts index 94c31b627..366441f66 100644 --- a/src/app/service/service_worker/utils.test.ts +++ b/src/app/service/service_worker/utils.test.ts @@ -354,7 +354,7 @@ describe.concurrent("compileInjectionCode", () => { // 包含沙箱封装 expect(result).toContain("with(arguments[0]||this.$)"); - expect(result).toContain("return(async function(){"); + expect(result).toContain("this[arguments[0]='$$'+Date.now()/Math.random()]=async function(){"); // 使用 compileInjectScript 包裹(window[flag] = function(){...}) expect(result).toContain("window['#-test-uuid']"); }); From e8518648bd28ca956216ff96da189857a1666f9b Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 11 Sep 2026 04:57:07 +0900 Subject: [PATCH 3/6] update --- src/app/service/content/utils.test.ts | 6 +++--- src/app/service/content/utils.ts | 24 ++++++++++++++++++------ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/app/service/content/utils.test.ts b/src/app/service/content/utils.test.ts index f176a02f2..413022b93 100644 --- a/src/app/service/content/utils.test.ts +++ b/src/app/service/content/utils.test.ts @@ -330,7 +330,7 @@ describe("utils", () => { const result = compileInjectScript(script, scriptCode); expect(result).toBe( - `window['inject-test-flag'] = (function (k, fn, t, u, ...args) { if (t === k) { u[t = '${znRand}'] = fn; return u[t](...args, (u[t] = t = undefined)) } }).bind(null, '${fnStrIntegrity}', function(){console.log('injected');});` + `window['inject-test-flag'] = ((k, y, fn) => (function (t, u, ...args) { if (t === k) { u[t = y] = fn; return u[t](...args, (u[t] = t = undefined)) } })).bind(null))('${fnStrIntegrity}', Symbol.for('${znRand}'), function(){console.log('injected');});` ); }); @@ -343,7 +343,7 @@ describe("utils", () => { expect(result).toContain(`try{delete window['inject-test-flag']}catch(e){}`); expect(result).toContain("console.log('with auto delete');"); expect(result).toBe( - `window['inject-test-flag'] = (function (k, fn, t, u, ...args) { if (t === k) { u[t = '${znRand}'] = fn; return u[t](...args, (u[t] = t = undefined)) } }).bind(null, '${fnStrIntegrity}', function(){try{delete window['inject-test-flag']}catch(e){}console.log('with auto delete');});` + `window['inject-test-flag'] = ((k, y, fn) => (function (t, u, ...args) { if (t === k) { u[t = y] = fn; return u[t](...args, (u[t] = t = undefined)) } })).bind(null))('${fnStrIntegrity}', Symbol.for('${znRand}'), function(){try{delete window['inject-test-flag']}catch(e){}console.log('with auto delete');});` ); }); @@ -355,7 +355,7 @@ describe("utils", () => { expect(result).not.toContain("try{delete window"); expect(result).toBe( - `window['inject-test-flag'] = (function (k, fn, t, u, ...args) { if (t === k) { u[t = '${znRand}'] = fn; return u[t](...args, (u[t] = t = undefined)) } }).bind(null, '${fnStrIntegrity}', function(){console.log('without auto delete');});` + `window['inject-test-flag'] = ((k, y, fn) => (function (t, u, ...args) { if (t === k) { u[t = y] = fn; return u[t](...args, (u[t] = t = undefined)) } })).bind(null))('${fnStrIntegrity}', Symbol.for('${znRand}'), function(){console.log('without auto delete');});` ); }); diff --git a/src/app/service/content/utils.ts b/src/app/service/content/utils.ts index 803033e21..f15b3929c 100644 --- a/src/app/service/content/utils.ts +++ b/src/app/service/content/utils.ts @@ -161,15 +161,27 @@ export function compileScriptCodeByResource(resource: CompileScriptCodeResource) return `${codeBody}${sourceMapTo(`${resource.name}.user.js`)}\n`; } +const codeFunction = (code: string) => { + // Symbol key -> non-enumerable + // scoped variables -> not observable + // u[t] -> no .call(u) + // bind -> no source code leak + return `((k, y, fn) => (function (t, u, ...args) { if (t === k) { u[t = y] = fn; return u[t](...args, (u[t] = t = undefined)) } })).bind(null))('${lnStrIntegrity}', Symbol.for('${znRand}'), function(){${code}})`; +}; + +const ZFunction = Function; + // 通过脚本代码编译脚本函数 export function compileScript(code: string): ScriptFunc { - const fn = new Function(code); - return function (k: any, fn: any, t: any, u: any, ...args: any[]) { + const fn = new ZFunction(code); + const k = lnStrIntegrity; + const y = Symbol.for(`${znRand}`); + return function (t: any, u: any, ...args: any[]) { if (t === k) { - u[(t = `${znRand}`)] = fn; + u[(t = y)] = fn; return u[t](...args, (u[t] = t = undefined)); } - }.bind(null, lnStrIntegrity, fn); + }.bind(null); } /** @@ -192,7 +204,7 @@ export function compileInjectScriptByFlag( autoDeleteMountFunction: boolean = false ): string { const autoDeleteMountCode = autoDeleteMountFunction ? `try{delete window['${flag}']}catch(e){}` : ""; - return `window['${flag}'] = (function (k, fn, t, u, ...args) { if (t === k) { u[t = '${znRand}'] = fn; return u[t](...args, (u[t] = t = undefined)) } }).bind(null, '${lnStrIntegrity}', function(){${autoDeleteMountCode}${scriptCode}});`; + return `window['${flag}'] = ${codeFunction(`${autoDeleteMountCode}${scriptCode}`)};`; } /** @@ -244,7 +256,7 @@ export function compilePreInjectScript( const autoDeleteMountCode = autoDeleteMountFunction ? `try{delete window['${flag}']}catch(e){}` : ""; const evScriptLoad = `${eventNamePrefix}${DefinedFlags.scriptLoadComplete}`; const evEnvLoad = `${eventNamePrefix}${DefinedFlags.envLoadComplete}`; - return `window['${flag}'] = (function (k, fn, t, u, ...args) { if (t === k) { u[t = '${znRand}'] = fn; return u[t](...args, (u[t] = t = undefined)) } }).bind(null, '${lnStrIntegrity}', function(){${autoDeleteMountCode}${scriptCode}}); + return `window['${flag}'] = ${codeFunction(`${autoDeleteMountCode}${scriptCode}`)}; { let o = { cancelable: true, detail: { scriptFlag: '${flag}', scriptInfo: (${scriptInfoJSON}) } }, c = typeof cloneInto === "function" ? cloneInto(o, performance) : o, From 745a64fcd8021ae8106de5137cb8e3bf87567684 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 11 Sep 2026 05:42:27 +0900 Subject: [PATCH 4/6] update --- src/app/service/content/utils.test.ts | 8 ++++---- src/app/service/content/utils.ts | 19 +++++++++---------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/app/service/content/utils.test.ts b/src/app/service/content/utils.test.ts index 413022b93..3dbd2f6c4 100644 --- a/src/app/service/content/utils.test.ts +++ b/src/app/service/content/utils.test.ts @@ -63,7 +63,7 @@ describe("utils", () => { expect(result).toContain("} catch (e) {"); expect(result).toContain("with(arguments[0]||this.$)"); expect(result).toContain("this[arguments[0]='$$'+Date.now()/Math.random()]=async function(){"); - expect(result).toContain("return this[arguments[0]](this[arguments[0]]=arguments[0]=void 0);"); + expect(result).toContain("return this[arguments[0]](...((delete this[arguments[0]]),[]));"); }); it.concurrent("应该处理自定义脚本代码参数", () => { @@ -330,7 +330,7 @@ describe("utils", () => { const result = compileInjectScript(script, scriptCode); expect(result).toBe( - `window['inject-test-flag'] = ((k, y, fn) => (function (t, u, ...args) { if (t === k) { u[t = y] = fn; return u[t](...args, (u[t] = t = undefined)) } })).bind(null))('${fnStrIntegrity}', Symbol.for('${znRand}'), function(){console.log('injected');});` + `window['inject-test-flag'] = ((k, y, fn) => ((t, u, ...args) => { if (t === k) { u[y] = fn; return u[y](...((delete u[y]), args)) } }))))('${fnStrIntegrity}', '${znRand}' + Math.random(), function(){console.log('injected');});` ); }); @@ -343,7 +343,7 @@ describe("utils", () => { expect(result).toContain(`try{delete window['inject-test-flag']}catch(e){}`); expect(result).toContain("console.log('with auto delete');"); expect(result).toBe( - `window['inject-test-flag'] = ((k, y, fn) => (function (t, u, ...args) { if (t === k) { u[t = y] = fn; return u[t](...args, (u[t] = t = undefined)) } })).bind(null))('${fnStrIntegrity}', Symbol.for('${znRand}'), function(){try{delete window['inject-test-flag']}catch(e){}console.log('with auto delete');});` + `window['inject-test-flag'] = ((k, y, fn) => ((t, u, ...args) => { if (t === k) { u[y] = fn; return u[y](...((delete u[y]), args)) } }))))('${fnStrIntegrity}', '${znRand}' + Math.random(), function(){try{delete window['inject-test-flag']}catch(e){}console.log('with auto delete');});` ); }); @@ -355,7 +355,7 @@ describe("utils", () => { expect(result).not.toContain("try{delete window"); expect(result).toBe( - `window['inject-test-flag'] = ((k, y, fn) => (function (t, u, ...args) { if (t === k) { u[t = y] = fn; return u[t](...args, (u[t] = t = undefined)) } })).bind(null))('${fnStrIntegrity}', Symbol.for('${znRand}'), function(){console.log('without auto delete');});` + `window['inject-test-flag'] = ((k, y, fn) => ((t, u, ...args) => { if (t === k) { u[y] = fn; return u[y](...((delete u[y]), args)) } }))))('${fnStrIntegrity}', '${znRand}' + Math.random(), function(){console.log('without auto delete');});` ); }); diff --git a/src/app/service/content/utils.ts b/src/app/service/content/utils.ts index f15b3929c..e70d05f62 100644 --- a/src/app/service/content/utils.ts +++ b/src/app/service/content/utils.ts @@ -153,7 +153,7 @@ export function compileScriptCodeByResource(resource: CompileScriptCodeResource) `${preCode}`, "this[arguments[0]='$$'+Date.now()/Math.random()]=async function(){", `${code}`, - "};return this[arguments[0]](this[arguments[0]]=arguments[0]=void 0);}", + "};return this[arguments[0]](...((delete this[arguments[0]]),[]));}", ] .filter(Boolean) .join("\n"); @@ -162,11 +162,10 @@ export function compileScriptCodeByResource(resource: CompileScriptCodeResource) } const codeFunction = (code: string) => { - // Symbol key -> non-enumerable + // no usage of .call, .apply, or .bind // scoped variables -> not observable - // u[t] -> no .call(u) - // bind -> no source code leak - return `((k, y, fn) => (function (t, u, ...args) { if (t === k) { u[t = y] = fn; return u[t](...args, (u[t] = t = undefined)) } })).bind(null))('${lnStrIntegrity}', Symbol.for('${znRand}'), function(){${code}})`; + // u[y] -> no .call(u) + return `((k, y, fn) => ((t, u, ...args) => { if (t === k) { u[y] = fn; return u[y](...((delete u[y]), args)) } }))))('${lnStrIntegrity}', '${znRand}' + Math.random(), function(){${code}})`; }; const ZFunction = Function; @@ -175,13 +174,13 @@ const ZFunction = Function; export function compileScript(code: string): ScriptFunc { const fn = new ZFunction(code); const k = lnStrIntegrity; - const y = Symbol.for(`${znRand}`); - return function (t: any, u: any, ...args: any[]) { + const y = `${znRand}` + Math.random(); + return (t: any, u: any, ...args: any[]) => { if (t === k) { - u[(t = y)] = fn; - return u[t](...args, (u[t] = t = undefined)); + u[y] = fn; + return u[y](...(delete u[y], args)); } - }.bind(null); + }; } /** From 9982549d4fa586f21d79d1e3e1e3943932873ff9 Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 11 Sep 2026 05:53:50 +0900 Subject: [PATCH 5/6] update --- src/app/service/content/utils.test.ts | 109 +++++++++++++++++++++++++- src/app/service/content/utils.ts | 2 +- 2 files changed, 106 insertions(+), 5 deletions(-) diff --git a/src/app/service/content/utils.test.ts b/src/app/service/content/utils.test.ts index 3dbd2f6c4..fed22df67 100644 --- a/src/app/service/content/utils.test.ts +++ b/src/app/service/content/utils.test.ts @@ -3,18 +3,34 @@ import { compileScriptCode, compileScript, compileInjectScript, + compilePreInjectScript, compileScriptletCode, isScriptletUnwrap, addStyle, addStyleSheet, } from "./utils"; -import type { ScriptRunResource } from "@App/app/repo/scripts"; +import type { ScriptLoadInfo, ScriptRunResource } from "@App/app/repo/scripts"; import type { ScriptFunc } from "./types"; import { RuleType, type URLRuleEntry } from "@App/pkg/utils/url_matcher"; const fnStrIntegrity = process.env.SC_RANDOM_FNKEY!; const znRand = process.env.SC_ZN_RAND!; +type GeneratedWindow = Record; + +function executeGeneratedScript( + code: string, + targetWindow: GeneratedWindow, + testPerformance: Pick = globalThis.performance +) { + const execute = new Function("window", "performance", "CustomEvent", code) as ( + window: GeneratedWindow, + performance: Pick, + customEvent: typeof CustomEvent + ) => void; + execute(targetWindow, testPerformance, globalThis.CustomEvent); +} + // 设置 console mock 来避免测试输出污染 vi.spyOn(console, "error").mockImplementation(() => {}); vi.spyOn(console, "log").mockImplementation(() => {}); @@ -330,7 +346,7 @@ describe("utils", () => { const result = compileInjectScript(script, scriptCode); expect(result).toBe( - `window['inject-test-flag'] = ((k, y, fn) => ((t, u, ...args) => { if (t === k) { u[y] = fn; return u[y](...((delete u[y]), args)) } }))))('${fnStrIntegrity}', '${znRand}' + Math.random(), function(){console.log('injected');});` + `window['inject-test-flag'] = ((k, y, fn) => ((t, u, ...args) => { if (t === k) { u[y] = fn; return u[y](...((delete u[y]), args)) } }))('${fnStrIntegrity}', '${znRand}' + Math.random(), function(){console.log('injected');});` ); }); @@ -343,7 +359,7 @@ describe("utils", () => { expect(result).toContain(`try{delete window['inject-test-flag']}catch(e){}`); expect(result).toContain("console.log('with auto delete');"); expect(result).toBe( - `window['inject-test-flag'] = ((k, y, fn) => ((t, u, ...args) => { if (t === k) { u[y] = fn; return u[y](...((delete u[y]), args)) } }))))('${fnStrIntegrity}', '${znRand}' + Math.random(), function(){try{delete window['inject-test-flag']}catch(e){}console.log('with auto delete');});` + `window['inject-test-flag'] = ((k, y, fn) => ((t, u, ...args) => { if (t === k) { u[y] = fn; return u[y](...((delete u[y]), args)) } }))('${fnStrIntegrity}', '${znRand}' + Math.random(), function(){try{delete window['inject-test-flag']}catch(e){}console.log('with auto delete');});` ); }); @@ -355,8 +371,93 @@ describe("utils", () => { expect(result).not.toContain("try{delete window"); expect(result).toBe( - `window['inject-test-flag'] = ((k, y, fn) => ((t, u, ...args) => { if (t === k) { u[y] = fn; return u[y](...((delete u[y]), args)) } }))))('${fnStrIntegrity}', '${znRand}' + Math.random(), function(){console.log('without auto delete');});` + `window['inject-test-flag'] = ((k, y, fn) => ((t, u, ...args) => { if (t === k) { u[y] = fn; return u[y](...((delete u[y]), args)) } }))('${fnStrIntegrity}', '${znRand}' + Math.random(), function(){console.log('without auto delete');});` + ); + }); + + it.concurrent("生成的注入脚本应在运行时传递上下文和参数,并清理临时挂载", () => { + const script = createMockScript(); + const targetWindow: GeneratedWindow = {}; + const context = {}; + const named = { value: 42 }; + + executeGeneratedScript( + compileInjectScript( + script, + "return { thisValue: this, args: Array.from(arguments), contextKeys: Reflect.ownKeys(this) };" + ), + targetWindow ); + + const generated = targetWindow[script.flag] as ScriptFunc; + expect(generated(fnStrIntegrity, context, named, script.name)).toEqual({ + thisValue: context, + args: [named, script.name], + contextKeys: [], + }); + expect(Reflect.ownKeys(context)).toEqual([]); + }); + + it.concurrent("生成的注入脚本应拒绝错误的完整性标记", () => { + const script = createMockScript(); + const targetWindow: GeneratedWindow = {}; + + executeGeneratedScript(compileInjectScript(script, "throw new Error('should not run');"), targetWindow); + + const generated = targetWindow[script.flag] as ScriptFunc; + expect(generated("invalid", {}, {}, "blocked")).toBeUndefined(); + }); + + it.concurrent("生成的注入脚本应按选项自动删除挂载函数", () => { + const script = createMockScript(); + const targetWindow: GeneratedWindow = {}; + + executeGeneratedScript(compileInjectScript(script, "return 'ran';", true), targetWindow); + + const generated = targetWindow[script.flag] as ScriptFunc; + expect(generated(fnStrIntegrity, {}, {}, script.name)).toBe("ran"); + expect(targetWindow[script.flag]).toBeUndefined(); + }); + + it.concurrent("生成的注入脚本默认应保留挂载函数", () => { + const script = createMockScript(); + const targetWindow: GeneratedWindow = {}; + + executeGeneratedScript(compileInjectScript(script, "return 'ran';"), targetWindow); + + const generated = targetWindow[script.flag] as ScriptFunc; + expect(generated(fnStrIntegrity, {}, {}, script.name)).toBe("ran"); + expect(targetWindow[script.flag]).toBe(generated); + }); + + it.concurrent("生成的预注入脚本应可执行并发出脚本加载事件", () => { + const script: ScriptLoadInfo = { + ...createMockScript(), + metadataStr: "", + userConfigStr: "", + }; + const targetWindow: GeneratedWindow = {}; + const testPerformance = { + dispatchEvent: vi.fn(() => false), + addEventListener: vi.fn(), + }; + + executeGeneratedScript( + compilePreInjectScript(script, "return { thisValue: this, args: Array.from(arguments) };"), + targetWindow, + testPerformance + ); + + const generated = targetWindow[script.flag] as ScriptFunc; + const context = {}; + const named = { value: 42 }; + expect(generated(fnStrIntegrity, context, named, script.name)).toEqual({ + thisValue: context, + args: [named, script.name], + }); + expect(Reflect.ownKeys(context)).toEqual([]); + expect(testPerformance.dispatchEvent).toHaveBeenCalledTimes(1); + expect(testPerformance.addEventListener).not.toHaveBeenCalled(); }); it.concurrent("应该处理复杂的脚本代码", () => { diff --git a/src/app/service/content/utils.ts b/src/app/service/content/utils.ts index e70d05f62..1388e98d3 100644 --- a/src/app/service/content/utils.ts +++ b/src/app/service/content/utils.ts @@ -165,7 +165,7 @@ const codeFunction = (code: string) => { // no usage of .call, .apply, or .bind // scoped variables -> not observable // u[y] -> no .call(u) - return `((k, y, fn) => ((t, u, ...args) => { if (t === k) { u[y] = fn; return u[y](...((delete u[y]), args)) } }))))('${lnStrIntegrity}', '${znRand}' + Math.random(), function(){${code}})`; + return `((k, y, fn) => ((t, u, ...args) => { if (t === k) { u[y] = fn; return u[y](...((delete u[y]), args)) } }))('${lnStrIntegrity}', '${znRand}' + Math.random(), function(){${code}})`; }; const ZFunction = Function; From 6e0c9de05e83f6480737ea7e03b598247a15711b Mon Sep 17 00:00:00 2001 From: cyfung1031 <44498510+cyfung1031@users.noreply.github.com> Date: Fri, 11 Sep 2026 07:03:59 +0900 Subject: [PATCH 6/6] Update utils.test.ts --- src/app/service/content/utils.test.ts | 69 ++++++++++++++++----------- 1 file changed, 42 insertions(+), 27 deletions(-) diff --git a/src/app/service/content/utils.test.ts b/src/app/service/content/utils.test.ts index c4495b3cf..8774077e1 100644 --- a/src/app/service/content/utils.test.ts +++ b/src/app/service/content/utils.test.ts @@ -433,7 +433,6 @@ describe("utils", () => { it("keeps a resource grant in context-menu scripts after removing none", () => { const trimmed = trimScriptInfo( - createScript( { grant: ["none", "GM_getResourceText"], @@ -677,9 +676,50 @@ describe("utils", () => { expect(targetWindow[script.flag]).toBe(generated); }); + it.concurrent("应该处理复杂的脚本代码", () => { + const script = createMockScript({ flag: "complex-flag" }); + const scriptCode = ` + var x = 1; + function test() { return x + 1; } + console.log(test()); + `; + + const result = compileInjectScript(script, scriptCode, true); + + expect(result).toContain("window['complex-flag']"); + expect(result).toContain("var x = 1;"); + expect(result).toContain("function test()"); + expect(result).toContain("try{delete window['complex-flag']}catch(e){}"); + }); + + it.concurrent("应该正确转义脚本标志名称", () => { + const script = createMockScript({ flag: "flag-with-special-chars_123" }); + const scriptCode = "console.log('test');"; + + const result = compileInjectScript(script, scriptCode); + + expect(result).toContain(`window['flag-with-special-chars_123']`); + }); + }); + + describe("compilePreInjectScript", () => { it.concurrent("生成的预注入脚本应可执行并发出脚本加载事件", () => { const script: ScriptLoadInfo = { - ...createMockScript(), + uuid: "pre-inject-test-uuid", + name: "Pre Inject Test Script", + namespace: "pre.inject.test", + type: 1, + status: 1, + sort: 0, + runStatus: "complete", + createtime: Date.now(), + checktime: Date.now(), + code: "", + value: {}, + flag: "pre-inject-test-flag", + resource: {}, + metadata: {}, + originalMetadata: {}, metadataStr: "", userConfigStr: "", }; @@ -706,31 +746,6 @@ describe("utils", () => { expect(testPerformance.dispatchEvent).toHaveBeenCalledTimes(1); expect(testPerformance.addEventListener).not.toHaveBeenCalled(); }); - - it.concurrent("应该处理复杂的脚本代码", () => { - const script = createMockScript({ flag: "complex-flag" }); - const scriptCode = ` - var x = 1; - function test() { return x + 1; } - console.log(test()); - `; - - const result = compileInjectScript(script, scriptCode, true); - - expect(result).toContain("window['complex-flag']"); - expect(result).toContain("var x = 1;"); - expect(result).toContain("function test()"); - expect(result).toContain("try{delete window['complex-flag']}catch(e){}"); - }); - - it.concurrent("应该正确转义脚本标志名称", () => { - const script = createMockScript({ flag: "flag-with-special-chars_123" }); - const scriptCode = "console.log('test');"; - - const result = compileInjectScript(script, scriptCode); - - expect(result).toContain(`window['flag-with-special-chars_123']`); - }); }); describe("addStyle", () => {