Skip to content

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

Open
cyfung1031 wants to merge 7 commits into
scriptscat:mainfrom
cyfung1031:pr/tm-compatbility-fix-for-cookie-in-GMxhr
Open

🐛 修复 GM_xmlhttpRequest 自定义 cookie 追加而非覆盖同名 cookie#1604
cyfung1031 wants to merge 7 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant