Skip to content

🐛 修复 GM_xmlhttpRequest 自定义 cookie 追加而非覆盖同名 cookie#1604

Merged
CodFrm merged 8 commits into
scriptscat:mainfrom
cyfung1031:pr/tm-compatbility-fix-for-cookie-in-GMxhr
Jul 20, 2026
Merged

🐛 修复 GM_xmlhttpRequest 自定义 cookie 追加而非覆盖同名 cookie#1604
CodFrm merged 8 commits into
scriptscat:mainfrom
cyfung1031:pr/tm-compatbility-fix-for-cookie-in-GMxhr

Conversation

@cyfung1031

@cyfung1031 cyfung1031 commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Checklist / 检查清单

  • Fixes GM_xmlhttpRequest custom cookie overwrite semantics / 已修复 GM_xmlhttpRequest 自定义 cookie 覆盖语义
  • Code reviewed by human / 代码通过人工检查
  • Changes tested / 已完成测试

背景

Tampermonkey tampermonkey/tampermonkey#2754 指出:GM_xmlhttpRequestcookie 参数应当覆盖同名的已有 cookie,而不是与其拼接发送。

ScriptCat 存在同样的问题:在非 anonymous 模式下,src/app/service/service_worker/gm_api/gm_api.tsbuildDNRRule 会先把脚本自定义的 cookie 字符串写入 headers["cookie"],再无条件地把 chrome.cookies.getAll() 取到的浏览器已存储 cookie 追加在后面,即使名称相同也不会覆盖,导致请求头出现 data=1; data=2 这样的重复 cookie 名。

Tampermonkey 后续为修复 #2754 引入了新的回归 tampermonkey/tampermonkey#2829:多个 cookie 被截断为只剩第一个。该回归目前 TM 尚未修复,但 ScriptCat 的实现不应重蹈覆辙。

另外,cookie 名称在规范上允许因 domain/path 不同而以多值形式共存于同一请求 URL(例如子网域与父网域各自设置了同名 cookie,两者都匹配同一个请求)。

本次改动

  • 新增纯函数 mergeCookieHeader(customCookie, storedCookies)src/app/service/service_worker/gm_api/gm_api.ts),按 cookie 名称合并浏览器已有 cookie 与脚本自定义的 cookie 参数:
    • 某个 cookie 名称若被脚本指定,完全以脚本指定的值覆盖浏览器该名称下的全部已有值(无论原本是 1 个还是多个);脚本指定的值本身也可以是同名多值,原样保留、不做去重(对应 TM #2754)。
    • 某个 cookie 名称若未被脚本指定,完全保留浏览器原样(无论是 0 个、1 个还是多个值均不改动),避免重蹈 TM #2829 的截断回归。
  • buildDNRRule 的非 anonymous 分支改为调用 mergeCookieHeader,替换原本的字符串拼接逻辑。
  • anonymous 分支(使用 DNR modifyHeaders 直接 set/remove 整个 Cookie 头,不查询浏览器已有 cookie)未受影响,本身已符合覆盖语义。
  • 针对单一 cookie 名称的「脚本指定状态(未指定/单一值/多个值)× 浏览器已有状态(无/单个/多个)」补齐完整的 3×3=9 种组合单元测试。
  • 新增 example/tests/gm_xhr_cookie_test.js:沿用 inject_content_test.js 的自动执行 + console.log 汇总风格(支持机制化检查),在真实浏览器中用 document.cookie 与真实 GM_xmlhttpRequest 复现 TM #2754 / #2829 的字面场景,并对照同一份 3×3 矩阵。

实现考虑

  • 判断依据只有「该 cookie 名称是否出现在脚本的 cookie 参数中」,不区分浏览器该名称下原本有几个值——只要脚本指定了,就是完全接管;没指定,就完全不动。这比按「已有同名 cookie 数量」区分覆盖/附加的做法更符合直觉,也更简单。
  • 逻辑抽成独立的纯函数,便于不依赖 chrome.cookies mock 直接做单元测试。
  • example/tests/gm_xhr_cookie_test.js 借由同名 cookie 设置不同 path//headers)在同一浏览器内制造合法的「同名多值」cookie,两者都会匹配到 https://httpbun.com/headers 请求,藉此在真实 Chrome cookie jar 下验证矩阵中「浏览器已有 2 个同名值」的分支。

已知限制

  • 仅覆盖 GM_xmlhttpRequestbuildDNRRule)路径;GM_cookie 相关 API 不受影响。
  • example/tests/gm_xhr_cookie_test.js 是手动验证脚本(依 docs/verification.md 的 in-page self-test 规范撰写),不是 CI 中运行的 Playwright E2E 用例。

建议审查重点

  • mergeCookieHeader 对空值 / 空字符串 / 无浏览器 cookie / 同名多值(脚本端与浏览器端)等边界情况的处理,参见新增的 3×3 组合测试。
  • buildDNRRule 中该分支对 headers["cookie"] 的赋值时机是否影响后续的 unsafe header 处理逻辑(第 761 行起的 for...of Object.entries(headers) 循环,cookie 属于 unsafeHeaders)。

参考

验证

  • npx vitest run src/app/service/service_worker/gm_api/gm_api.test.ts — 40 passed
  • npx vitest run src/app/service/service_worker/gm_api — 60 passed(4 test files,含 blast-radius 检查)
  • npx tsc --noEmit -p . — 无报错
  • npx eslint src/app/service/service_worker/gm_api/gm_api.ts src/app/service/service_worker/gm_api/gm_api.test.ts — 无报错
  • 在最初还原修复前先运行新增的 mergeCookieHeader 用例确认因函数不存在而失败,验证测试确实覆盖了该问题,然后重新应用修复
  • example/tests/gm_xhr_cookie_test.js:语法检查通过(node --check);尚未在真实加载的扩展中手动运行(需要人工在浏览器中安装并触发 https://httpbun.com/*?GM_XHR_COOKIE_TEST_SC 页面),供审查者/维护者复核用

cyfung1031 and others added 4 commits July 18, 2026 02:49
TM兼容: 脚本传入的 cookie 参数(非 anonymous)此前会与浏览器已存储的同名
cookie 直接拼接发送(如 data=1; data=2;),与 Tampermonkey 的覆盖语义不一致
(Tampermonkey/tampermonkey#2754)。改为按 cookie 名称合并:脚本自定义值覆盖
同名的已有 cookie,不同名的 cookie 全部保留,避免重蹈 TM 修复该问题时引入的
多 cookie 截断回归 (Tampermonkey/tampermonkey#2829)。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
cookie 名称在规范上允许因 domain/path 不同而以多值形式共存(例如同一请求
URL 同时匹配到两个不同 scope、但同名的已有 cookie)。此前的覆盖逻辑只要
脚本自定义了该名称就会丢弃全部已有同名值,会把这种合法的多值场景错误地
压缩成单一值。

改为:仅当已有同名 cookie 唯一存在一个时才视为可被脚本自定义值覆盖;
存在多个同名值时无法判断脚本想覆盖哪一个,改为全部保留、脚本自定义值
附加在前面,不丢弃任何一个已有值。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
改回更简单直接的规则:某个 cookie 名称只要被脚本的 cookie 参数指定,
就完全覆盖浏览器该名称下的全部已有值(无论原本是 1 个还是多个);
未被指定的名称则完全保留原样(无论是 0 个、1 个还是多个)。脚本指定
的值本身也可以是同名多值,原样保留、不做去重。

上一版按「已有同名 cookie 是否恰好唯一一个」区分覆盖/附加的做法过于
复杂,且与「脚本明确指定即完全接管该名称」的直觉不符。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
针对单一 cookie 名称,脚本指定状态(未指定/单一值/多个值)与浏览器已有
状态(无/单个/多个)各有 3 种,共 3×3=9 种组合,逐一断言覆盖/保留行为
符合预期规则,避免遗漏边界组合。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@cyfung1031

cyfung1031 commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator Author

manually check with the following scripts:

// ==UserScript==
// @name         GM_xmlhttpRequest cookie test - tampermonkey/tampermonkey#2754
// @namespace    http://tampermonkey.net/
// @version      2026-04-15
// @description  try to take over the world!
// @author       AA
// @match        https://httpbin.org/*
// @grant        GM_xmlhttpRequest
// ==/UserScript==

document.cookie = `data=1; max-age=86400;`;

document.body.onclick = () => {
  GM_xmlhttpRequest({
    url: 'https://httpbin.org/headers',
    cookie: 'data=2',
    responseType: 'json',
    onload({ response }) {
      const result = `${response?.headers?.Cookie}`;
      console.log({ result, ok: result.includes("data=2") && !result.replace("data=2", "").match(/data=/) });
    }
  });
}

and

// ==UserScript==
// @name         GM_xmlhttpRequest cookie test - tampermonkey/tampermonkey#2829
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  Test with httpbin
// @author       AA
// @match        https://httpbin.org/*
// @grant        GM_xmlhttpRequest
// ==/UserScript==

(function() {
    GM_xmlhttpRequest({
      url: 'https://httpbin.org/headers',
      cookie: 'data1=1; data2=2',
      // anonymous: true,
      responseType: 'json',
      onload({ response }) {
        const result = `${response?.headers?.Cookie}`;
        console.log({ result, ok: result.includes("data1=1; data2=2") && !result.replace("data1=", "").replace("data2=", "").match(/data[12]=/) });
      },
    });
}());

cyfung1031 and others added 3 commits July 18, 2026 03:49
沿用 example/tests/inject_content_test.js 的自动执行 + console.log 汇总
(总计/通过/失败)风格,支持机制化检查(docs/verification.md 描述的
in-page self-test pattern)。

覆盖:
- Tampermonkey/tampermonkey#2754 的字面复现(data=1 被 cookie: 'data=2'
  完全覆盖,而非拼接成 data=1; data=2)
- Tampermonkey/tampermonkey#2829 的字面复现(cookie: 'data1=1; data2=2'
  两个不同名 cookie 都应送出,不应被截断成只剩 data1)
- 浏览器已有状态(无/单值/同名多值)× 脚本指定状态(未指定/单值/多值)
  的完整 3x3 矩阵,通过 document.cookie 以不同 path 属性制造同名多值
  cookie 来验证,并对照 gm_api.test.ts 中的 mergeCookieHeader 单元测试

Testing source references: scriptscat#1604 (comment)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@cyfung1031 cyfung1031 added the P1 🔥 重要但是不紧急的内容 label Jul 18, 2026
@cyfung1031 cyfung1031 added this to the 2026七月 Milestone milestone Jul 18, 2026
@cyfung1031

Copy link
Copy Markdown
Collaborator Author

tested by me. example/tests/gm_xhr_cookie_test.js‎ passed.

@CodFrm
CodFrm merged commit 95e64a8 into scriptscat:main Jul 20, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P1 🔥 重要但是不紧急的内容

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants