Skip to content
Merged
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
75 changes: 66 additions & 9 deletions src/app/service/content/create_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,38 @@ export const createContext = (

const noEval = false;

// 判断是否应该将函数绑定到global
// 取得原生函数代码表示
const getNativeCodeSeg = () => {
const k = "propertyIsEnumerable"; // 选用 Object.propertyIsEnumerable 取得原生函数代码表示
const codeSeg = `${Object[k]}`;
const idx1 = codeSeg.indexOf(k);
const idx2 = codeSeg.indexOf("()");
const idx3 = codeSeg.lastIndexOf("(");
if (idx1 > 0 && idx2 > 0 && idx3 === idx2) {
return codeSeg.substring(idx1 + k.length);
}
return "";
};

const nativeCodeSeg = getNativeCodeSeg();

// 判断是否应该将函数绑定到global (原生函数)
const shouldFnBind = (f: any) => {
if (typeof f !== "function") return false;
// 函数有 prototype 即为 Class
if ("prototype" in f) return false; // 避免getter, 使用 in operator (注意, nodeJS的测试环境有异)
// window中的函式,大写开头不用于直接呼叫 (例如NodeFilter)
const { name } = f;
// 要求函数名字小写字头 能筛选掉 NodeFilter 之类 Interface ( 大写开头不用于直接呼叫 )
// 要求函数名字不包含空白 能筛选掉 已经this绑定函数
const { name } = f as typeof Function.prototype;
if (!name) return false;
const e = name.charCodeAt(0);
return e >= 97 && e <= 122;
if (e >= 97 && e <= 122 && !name.includes(" ")) {
// 为避免浏览器插件封装了 原生函数,需要进行 toString 测试
if (nativeCodeSeg.length && `${f}`.endsWith(`${name}${nativeCodeSeg}`)) {
return true;
}
}
return false;
};

type ForEachCallback<T> = (value: T, index: number, array: T[]) => void;
Expand Down Expand Up @@ -145,12 +168,15 @@ getAllPropertyDescriptors(global, ([key, desc]) => {

// 替换 function 的 this 为 实际的 global window
// 例:父类的 addEventListener
// 对于构造函数和类(有 prototype 属性),shouldFnBind 会返回 false,跳过绑定
// 因此被封装的属性,会略过封装层,继续向父类寻找原生属性
if (shouldFnBind(value)) {
const boundValue = value.bind(global);
overridedDescs[key] = {
...desc,
value: boundValue,
};
descsCache.add(key); // 必须:子类属性覆盖父类属性
}
} else {
if (desc.configurable && desc.get && desc.set && desc.enumerable && key.startsWith("on")) {
Expand All @@ -166,6 +192,7 @@ getAllPropertyDescriptors(global, ([key, desc]) => {
get: desc?.get?.bind(global),
set: desc?.set?.bind(global),
};
descsCache.add(key); // 必须:子类属性覆盖父类属性
}
}
}
Expand All @@ -176,12 +203,42 @@ descsCache.clear(); // 内存释放
// OwnPropertyDescriptor定义 为 原OwnPropertyDescriptor定义 (DragEvent, MouseEvent, RegExp, EventTarget, JSON等)
// + 覆盖定义 (document, location, setTimeout, setInterval, addEventListener 等)
// sharedInitCopy: ScriptCat脚本共通使用
const sharedInitCopy = Object.create(null, {
...initOwnDescs,
...overridedDescs,
// Symbol.toStringTag设置为 Window
[Symbol.toStringTag]: { value: "Window", writable: false, enumerable: false, configurable: true },

const USE_PSEUDO_WINDOW = true; // 日后或能设置使 ScriptCat的沙盒 window 能以 name / id 存取页面元素

class PseudoWindow {}
const PseudoWindowPrototype = PseudoWindow.prototype;
Object.defineProperty(PseudoWindowPrototype, Symbol.toStringTag, {
//@ts-ignore
value: global[Symbol.toStringTag],
writable: false,
enumerable: false,
configurable: true,
});
Object.defineProperty(PseudoWindowPrototype, "constructor", {
value: global.constructor,
writable: false,
enumerable: false,
configurable: true,
});
Object.defineProperty(PseudoWindowPrototype, "__proto__", {
//@ts-ignore
value: global.__proto__,
writable: false,
enumerable: false,
configurable: true,
});

const sharedInitCopy = USE_PSEUDO_WINDOW
? Object.create(null, {
...Object.getOwnPropertyDescriptors(PseudoWindowPrototype),
...initOwnDescs,
...overridedDescs,
})
: Object.create(Object.getPrototypeOf(global), {
...initOwnDescs,
...overridedDescs,
});

type GMWorldContext = typeof globalThis & Record<PropertyKey, any>;

Expand Down
58 changes: 43 additions & 15 deletions src/app/service/content/exec_script.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ describe.concurrent("sandbox", () => {
});
});

describe.concurrent("this", () => {
describe("this", () => {
it("onload", async () => {
// null确认
global.onload = null;
Expand Down Expand Up @@ -234,7 +234,7 @@ describe("沙盒环境测试", async () => {

const _global = <any>global;

scriptRes2.code = `return [this, window];`;
scriptRes2.code = `return [window, this];`;
sandboxExec.scriptFunc = compileScript(compileScriptCode(scriptRes2));
const [_win, _this] = await sandboxExec.exec();
expect(_win).toEqual(expect.any(Object));
Expand Down Expand Up @@ -272,17 +272,17 @@ describe("沙盒环境测试", async () => {
expect(_global["test_md5"]).toEqual(undefined);
});

describe.concurrent("set window.onload null", () => {
it.concurrent("初始状态确认", () => {
describe("set window.onload null", () => {
it("初始状态确认", () => {
// null确认
_this["onload"] = null;
_global["onload"] = null;
expect(_this["onload"]).toBeNull();
expect(_global["onload"]).toBeNull();
});

describe.concurrent("沙盒环境 onload 设置", () => {
it.concurrent("设置 _this.onload 不影响 global.onload", () => {
describe("沙盒环境 onload 设置", () => {
it("设置 _this.onload 不影响 global.onload", () => {
const mockFn = vi.fn();
_this["onload"] = function thisOnLoad() {
mockFn();
Expand All @@ -291,15 +291,16 @@ describe("沙盒环境测试", async () => {
expect(_global["onload"]).toBeNull();
});

it.concurrent("验证 onload 事件调用", () => {
const mockFn = vi.fn();
_this["onload"] = function thisOnLoad() {
mockFn();
};
// 验证调用
global.dispatchEvent(new Event("load"));
expect(mockFn).toHaveBeenCalledTimes(1);
});
// 在模拟环境无法测试:在模拟环境模拟 dispatchEvent 呼叫 this.onload 没有意义
// it("验证 onload 事件调用", () => {
// const mockFn = vi.fn();
// _this["onload"] = function thisOnLoad() {
// mockFn();
// };
// // 验证调用
// global.dispatchEvent(new Event("load"));
// expect(mockFn).toHaveBeenCalledTimes(1);
// });

// 在模拟环境无法测试:在实际操作中和TM一致
// 在非拦截式沙盒裡删除 沙盒onload 后,会取得页面的真onload
Expand Down Expand Up @@ -440,6 +441,33 @@ describe("沙盒环境测试", async () => {
expect(Object.prototype.hasOwnProperty.call(_this, "test")).toEqual(false);
});

// https://github.com/scriptscat/scriptcat/issues/962
// window.constructor === Window
// window instanceof Window === false
it.concurrent("TM Sandbox Window", () => {
const window = global;
//@ts-ignore
expect(_win.PERSISTENT === window.PERSISTENT).toEqual(true);
//@ts-ignore
expect(_win.TEMPORARY === window.TEMPORARY).toEqual(true);
//@ts-ignore
expect(_win.constructor === window.constructor).toEqual(true);
//@ts-ignore
expect(_win.__proto__ === window.__proto__).toEqual(true);
//@ts-ignore
expect(typeof window.constructor === "function").toEqual(true);
//@ts-ignore
expect(typeof _win.constructor === "function").toEqual(true);
//@ts-ignore
expect(window instanceof window.constructor === true).toEqual(true);
//@ts-ignore
expect(_win instanceof window.constructor === false).toEqual(true);
//@ts-ignore
expect(_win.addEventListener !== window.addEventListener).toEqual(true);
//@ts-ignore
expect(Object.getPrototypeOf(_win) === null).toEqual(true);
});

it.concurrent("特殊关键字不能穿透沙盒", async () => {
expect(_global["define"]).toEqual("特殊关键字不能穿透沙盒");
expect(_this["define"]).toBeUndefined();
Expand Down
67 changes: 67 additions & 0 deletions tests/vitest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,70 @@ chromeMock.runtime.getURL = vi.fn().mockImplementation((path: string) => {

const isPrimitive = (x: any) => x !== Object(x);

// Window.prototype[Symbol.toStringTag] = "Window"
Object.defineProperty(Object.getPrototypeOf(global), Symbol.toStringTag, {
value: "Window",
writable: false,
enumerable: false,
configurable: true,
});
// 先改变 global[Symbol.toStringTag] 定义
Object.defineProperty(global, Symbol.toStringTag, {
value: undefined,
writable: false,
enumerable: false,
configurable: true,
});
// 删除 global 表面的 property,使用 Window.prototype[Symbol.toStringTag]
//@ts-expect-error
if (!global[Symbol.toStringTag]) delete global[Symbol.toStringTag];

const gblAddEventListener = Object.getPrototypeOf(global).addEventListener || global.addEventListener;
const gblRemoveEventListener = Object.getPrototypeOf(global).removeEventListener || global.removeEventListener;
class EventTargetE {
addEventListener(a: any, b: any, ...args: any[]) {
return gblAddEventListener.call(this, a, b, ...args);
}
removeEventListener(a: any, ...args: any[]) {
return gblRemoveEventListener.call(this, a, ...args);
}
}
// 为了确保全局 addEventListener/removeEventListener 行为符合预期,需要彻底移除 global 及其原型链上的相关属性,
// 然后在原型链上重新定义。此处操作较为复杂,务必小心维护。
// 先安全地删除 global 上的 addEventListener/removeEventListener
if (Object.getOwnPropertyDescriptor(global, "addEventListener")) {
// @ts-ignore
delete global.addEventListener;
}
if (Object.getOwnPropertyDescriptor(global, "removeEventListener")) {
// @ts-ignore
delete global.removeEventListener;
}
// 再删除 global 的原型上的属性
const globalProto = Object.getPrototypeOf(global);
if (globalProto && Object.getOwnPropertyDescriptor(globalProto, "addEventListener")) {
// @ts-ignore
delete globalProto.addEventListener;
}
if (globalProto && Object.getOwnPropertyDescriptor(globalProto, "removeEventListener")) {
// @ts-ignore
delete globalProto.removeEventListener;
}
// 继续向上查找一层原型(防御性检查)
const globalProtoProto = globalProto && Object.getPrototypeOf(globalProto);
if (globalProtoProto && Object.getOwnPropertyDescriptor(globalProtoProto, "addEventListener")) {
// @ts-ignore
delete globalProtoProto.addEventListener;
}
if (globalProtoProto && Object.getOwnPropertyDescriptor(globalProtoProto, "removeEventListener")) {
// @ts-ignore
delete globalProtoProto.removeEventListener;
}
// 在 global 的原型上重新定义方法
if (globalProto) {
globalProto.addEventListener = EventTargetE.prototype.addEventListener;
globalProto.removeEventListener = EventTargetE.prototype.removeEventListener;
}
if (!("onanimationstart" in global)) {
// Define or mock the global handler
let val: any = null;
Expand Down Expand Up @@ -115,6 +179,9 @@ Object.assign(global, {
return this.setTimeout(...args);
},
});
//@ts-ignore 强行修改 setTimeoutForTest toString 为 原生代码显示
global.setTimeoutForTest.toString = () =>
`${Object.propertyIsEnumerable}`.replace("propertyIsEnumerable", "setTimeoutForTest");

vi.stubGlobal("sandboxTestValue", "sandboxTestValue");
vi.stubGlobal("sandboxTestValue2", "sandboxTestValue2");
Expand Down
Loading