Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 80 additions & 1 deletion src/app/service/content/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ 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";
import { DefinedFlags } from "../service_worker/runtime.consts";
import { ScriptEnvTag } from "@Packages/message/consts";

// 设置 console mock 来避免测试输出污染
vi.spyOn(console, "error").mockImplementation(() => {});
Expand Down Expand Up @@ -563,6 +566,82 @@ describe("utils", () => {
});
});

describe.concurrent("compilePreInjectScript", () => {
const createMockScript = (overrides: Partial<ScriptLoadInfo> = {}): ScriptLoadInfo => ({
uuid: "test-uuid",
name: "Early Start Script",
namespace: "test.namespace",
type: 1,
status: 1,
sort: 0,
runStatus: "complete",
createtime: Date.now(),
checktime: Date.now(),
code: "console.log('early');",
value: {},
flag: "test-flag",
resource: {},
metadata: { "early-start": [""], "run-at": ["document-start"] },
originalMetadata: {},
metadataStr: "",
userConfigStr: "",
...overrides,
});

it.concurrent("页面篡改 performance 实例方法时仍能发出加载事件", () => {
const script = createMockScript();
const code = compilePreInjectScript(script, script.code);
const fakeWindow: Record<string, unknown> = {};
class FakeCustomEvent {
defaultPrevented = false;

constructor(
public type: string,
public init: CustomEventInit
) {}

preventDefault() {
this.defaultPrevented = true;
}
}
class FakeEventTarget {
listeners = new Map<string, Array<(ev: FakeCustomEvent) => void>>();

dispatchEvent(ev: FakeCustomEvent) {
this.listeners.get(ev.type)?.forEach((listener) => listener(ev));
return !ev.defaultPrevented;
}

addEventListener(type: string, listener: (ev: FakeCustomEvent) => void) {
this.listeners.set(type, [...(this.listeners.get(type) || []), listener]);
}
}
const fakePerformance = new FakeEventTarget() as FakeEventTarget & {
dispatchEvent: FakeEventTarget["dispatchEvent"];
addEventListener: FakeEventTarget["addEventListener"];
};
const eventName = `evt${process.env.SC_RANDOM_KEY}.${ScriptEnvTag.inject}${DefinedFlags.scriptLoadComplete}`;
const listener = vi.fn((ev: FakeCustomEvent) => ev.preventDefault());
const safeDispatchEvent = vi.spyOn(FakeEventTarget.prototype, "dispatchEvent");

FakeEventTarget.prototype.addEventListener.call(fakePerformance, eventName, listener);
fakePerformance.dispatchEvent = vi.fn(() => true);
fakePerformance.addEventListener = vi.fn();

new Function("window", "performance", "CustomEvent", "EventTarget", code)(
fakeWindow,
fakePerformance,
FakeCustomEvent,
FakeEventTarget
);

expect(safeDispatchEvent).toHaveBeenCalledTimes(1);
expect(safeDispatchEvent.mock.calls[0][0].type).toBe(eventName);
expect(listener).toHaveBeenCalledTimes(1);
expect(fakePerformance.dispatchEvent).not.toHaveBeenCalled();
});
});

describe.concurrent("compileScriptletCode", () => {
const createMockScriptRes = (
overrides: Partial<ScriptRunResource> = {},
Expand Down
11 changes: 8 additions & 3 deletions src/app/service/content/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,13 +234,18 @@ 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 `{
const et = EventTarget.prototype,
dispatchEvent = et.dispatchEvent,
addEventListener = et.addEventListener;
window['${flag}'] = function(){${autoDeleteMountCode}${scriptCode}};
{
let o = { cancelable: true, detail: { scriptFlag: '${flag}', scriptInfo: (${scriptInfoJSON}) } },
c = typeof cloneInto === "function" ? cloneInto(o, performance) : o,
f = () => performance.dispatchEvent(new CustomEvent('${evScriptLoad}', c)),
f = () => dispatchEvent.call(performance, new CustomEvent('${evScriptLoad}', c)),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

当时我在写这个就意识到有被修改的问题,但我不觉得要处理。它不像window那样会失效。(没有沙盒替换) 假设能被改后也能正常执行吧。

如果你要仔细去想,Function.prototype的call和apply也可以被污染。

needWait = f();
if (needWait) performance.addEventListener('${evEnvLoad}', f, { once: true });
if (needWait) addEventListener.call(performance, '${evEnvLoad}', f, { once: true });
}
}
`;
}
Expand Down
Loading