Skip to content

Commit 69d98e2

Browse files
CodFrmcyfung1031
andauthored
🐛 修复 Firefox 中 GM_cookie 无法获取 cookie 的问题 (#1319)
* 🔒 修复 CustomEvent 通信密钥泄露漏洞 在模块顶层保存原生 CustomEvent、MouseEvent、dispatchEvent、addEventListener 引用, 防止页面脚本通过 hook 全局构造函数窃取 IPC 通信密钥,伪造 GM_* API 调用。 * 代码对齐最新做法 * 修正错误测试代码 * 🐛 修复 Firefox 中 GM_cookie 无法获取 cookie 的问题 #1187 * 处理jest问题 * 处理jest问题 * 🐛 GM_cookie 未指定 url/domain 时自动使用当前页面 URL * typescript * cookieQuery -> cookieParams * typo * 补充单元测试 * Update cookie_params.test.ts --------- Co-authored-by: cyfung1031 <44498510+cyfung1031@users.noreply.github.com>
1 parent c7ed5f9 commit 69d98e2

5 files changed

Lines changed: 165 additions & 8 deletions

File tree

jest.setup.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
process.env.VI_TESTING = "true";
2+
13
// Define / mock your globals here
24
global.fetch = () => {
35
return Promise.reject(new Error("not implemented"));

src/app/message/common.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
export const MouseEventClone = MouseEvent;
33
export const CustomEventClone = CustomEvent;
44

5-
const performanceClone = (process.env.VI_TESTING === "true" ? new EventTarget() : performance) as Performance;
5+
const performanceClone = (typeof process !== "undefined" && process.env.VI_TESTING === "true"
6+
? new EventTarget()
7+
: performance) as Performance;
68

79
// 避免页面载入后改动 EventTarget.prototype 的方法导致消息传递失败
810
export const pageDispatchEvent = performanceClone.dispatchEvent.bind(performanceClone);
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { cookieParams } from "./cookie_params";
2+
3+
// 用于模拟 Firefox 环境的辅助函数
4+
const mockFirefox = () => {
5+
(globalThis as any).mozInnerScreenX = 0;
6+
};
7+
8+
const mockNonFirefox = () => {
9+
delete (globalThis as any).mozInnerScreenX;
10+
};
11+
12+
describe("cookieParams", () => {
13+
14+
afterEach(() => {
15+
mockNonFirefox();
16+
});
17+
18+
describe("undefined 过滤", () => {
19+
it("从参数中移除 undefined 值", () => {
20+
const result = cookieParams({ domain: "example.com", name: undefined });
21+
expect(result).toEqual({ domain: "example.com" });
22+
});
23+
24+
it("保留 null 和 false 值", () => {
25+
const result = cookieParams({ domain: null, secure: false, name: undefined });
26+
expect(result).toEqual({ domain: null, secure: false });
27+
});
28+
29+
it("保留空字符串值", () => {
30+
const result = cookieParams({ domain: "", name: "test" });
31+
expect(result).toEqual({ domain: "", name: "test" });
32+
});
33+
34+
it("当所有值都是 undefined 时返回空对象", () => {
35+
const result = cookieParams({ domain: undefined, name: undefined });
36+
expect(result).toEqual({});
37+
});
38+
});
39+
40+
it("过滤掉 undefined 属性", () => {
41+
const result = cookieParams({
42+
url: "https://example.com",
43+
domain: undefined,
44+
name: undefined,
45+
path: "/",
46+
secure: undefined,
47+
});
48+
expect(result).toEqual({ url: "https://example.com", path: "/" });
49+
expect("domain" in result).toBe(false);
50+
expect("name" in result).toBe(false);
51+
expect("secure" in result).toBe(false);
52+
});
53+
54+
it("保留所有非 undefined 的属性(包括 null 和 false)", () => {
55+
const result = cookieParams({
56+
url: "https://example.com",
57+
secure: false,
58+
session: false,
59+
storeId: null as unknown as string,
60+
});
61+
expect(result).toEqual({
62+
url: "https://example.com",
63+
secure: false,
64+
session: false,
65+
storeId: null,
66+
});
67+
});
68+
69+
describe("Firefox:注入 firstPartyDomain", () => {
70+
it("在 Firefox 中注入 firstPartyDomain: null(情况1)", () => {
71+
mockFirefox();
72+
const result = cookieParams({ domain: "example.com" });
73+
expect(result).toEqual({ domain: "example.com", firstPartyDomain: null });
74+
});
75+
76+
it("在 Firefox 中注入 firstPartyDomain: null(情况2)", () => {
77+
mockFirefox();
78+
const result = cookieParams({ url: "https://example.com" });
79+
expect(result).toEqual({ url: "https://example.com", firstPartyDomain: null });
80+
});
81+
82+
it("在 Firefox 中覆盖已有的 firstPartyDomain(情况1)", () => {
83+
mockFirefox();
84+
const result = cookieParams({ domain: "example.com", firstPartyDomain: "other.com" });
85+
expect(result.firstPartyDomain).toBeNull();
86+
});
87+
88+
it("在 Firefox 中覆盖已有的 firstPartyDomain(情况2)", () => {
89+
mockFirefox();
90+
const result = cookieParams({ url: "https://example.com", firstPartyDomain: "other.com" });
91+
expect(result.firstPartyDomain).toBeNull();
92+
});
93+
94+
it("在非 Firefox 环境下不注入 firstPartyDomain(情况1)", () => {
95+
mockNonFirefox();
96+
const result = cookieParams({ domain: "example.com" });
97+
expect(result).not.toHaveProperty("firstPartyDomain");
98+
});
99+
100+
it("在非 Firefox 环境下不注入 firstPartyDomain(情况2)", () => {
101+
mockNonFirefox();
102+
const result = cookieParams({ url: "https://example.com" });
103+
expect(result).not.toHaveProperty("firstPartyDomain");
104+
});
105+
});
106+
107+
describe("真实 GM_cookie 使用场景", () => {
108+
it("处理带有可选字段的列表查询", () => {
109+
const result = cookieParams({
110+
domain: "example.com",
111+
name: undefined,
112+
path: undefined,
113+
secure: true,
114+
session: undefined,
115+
url: undefined,
116+
storeId: undefined,
117+
});
118+
expect(result).toEqual({ domain: "example.com", secure: true });
119+
});
120+
121+
it("处理删除查询", () => {
122+
const result = cookieParams({
123+
name: "session",
124+
url: "https://example.com",
125+
storeId: undefined,
126+
});
127+
expect(result).toEqual({ name: "session", url: "https://example.com" });
128+
});
129+
});
130+
131+
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// 过滤掉 undefined 属性,避免 Firefox cookies API 对 undefined 值的处理差异
2+
// Firefox 还需要 firstPartyDomain: null 以在 First-Party Isolation 开启时返回所有 cookie
3+
export function cookieParams<T extends { [key: string]: unknown; firstPartyDomain?: any; }>(params: T): T {
4+
// @ts-ignore
5+
const isFirefox = typeof mozInnerScreenX !== "undefined";
6+
const cleaned = Object.fromEntries(
7+
Object.entries(params).filter(([, v]) => v !== undefined)
8+
) as T;
9+
if (isFirefox) {
10+
cleaned.firstPartyDomain = null;
11+
}
12+
return cleaned;
13+
}

src/runtime/background/gm_api.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import {
3131
setXhrHeader,
3232
} from "./utils";
3333

34+
import { cookieParams } from "./cookie_params";
35+
3436
// GMApi,处理脚本的GM API调用请求
3537

3638
export type MessageRequest = {
@@ -669,6 +671,10 @@ export default class GMApi {
669671
return Promise.resolve(true);
670672
}
671673
const detail = <GMTypes.CookieDetails>request.params[1];
674+
// 未指定 url 和 domain 时,自动使用当前页面的 URL(兼容 Tampermonkey 行为)
675+
if (!detail.url && !detail.domain && request.sender.url) {
676+
detail.url = request.sender.url;
677+
}
672678
if (!detail.url && !detail.domain) {
673679
return Promise.reject(new Error("there must be one of url or domain"));
674680
}
@@ -735,7 +741,10 @@ export default class GMApi {
735741
});
736742
return;
737743
}
738-
// url或者域名不能为空
744+
// 未指定 url 和 domain 时,自动使用当前页面的 URL(兼容 Tampermonkey 行为)
745+
if (!detail.url && !detail.domain && request.sender.url) {
746+
detail.url = request.sender.url;
747+
}
739748
if (detail.url) {
740749
detail.url = detail.url.trim();
741750
}
@@ -749,15 +758,15 @@ export default class GMApi {
749758
switch (param[0]) {
750759
case "list": {
751760
chrome.cookies.getAll(
752-
{
761+
cookieParams({
753762
domain: detail.domain,
754763
name: detail.name,
755764
path: detail.path,
756765
secure: detail.secure,
757766
session: detail.session,
758767
url: detail.url,
759768
storeId: detail.storeId,
760-
},
769+
}),
761770
(cookies) => {
762771
resolve(cookies);
763772
}
@@ -770,11 +779,11 @@ export default class GMApi {
770779
return;
771780
}
772781
chrome.cookies.remove(
773-
{
782+
cookieParams({
774783
name: detail.name,
775784
url: detail.url,
776785
storeId: detail.storeId,
777-
},
786+
}),
778787
() => {
779788
resolve(undefined);
780789
}
@@ -787,7 +796,7 @@ export default class GMApi {
787796
return;
788797
}
789798
chrome.cookies.set(
790-
{
799+
cookieParams({
791800
url: detail.url,
792801
name: detail.name,
793802
domain: detail.domain,
@@ -797,7 +806,7 @@ export default class GMApi {
797806
httpOnly: detail.httpOnly,
798807
secure: detail.secure,
799808
storeId: detail.storeId,
800-
},
809+
}),
801810
() => {
802811
resolve(undefined);
803812
}

0 commit comments

Comments
 (0)