跳到主要内容

5 篇博文 含有标签「Chrome插件」

查看所有标签

Chrome 扩展消息被意外接收?postMessage targetOrigin 用星号的跨域泄露风险

· 阅读需 3 分钟

在为客户开发 Chrome 扩展数据采集工具时遇到此问题,记录根因与解法。

TL;DR

window.postMessage(data, '*') 会把消息广播给页面中所有 frame,包括第三方 iframe。如果你的 Chrome 扩展通过 postMessage 传递用户数据(如 memberId、业务报表),任何嵌入页面的 iframe 都能监听到。把 '*' 改为 window.location.origin 即可精确限定接收方。

问题现象

Chrome 扩展的采集脚本通过 postMessage 将电商数据传递给 content script:

// ❌ 不安全:消息广播到所有 frame
window.postMessage({
type: 'CCL_SHOP_REPORT_DAILY',
memberId: 'b2b-2214126315258ad300', // 用户 ID
rows: [{ uv: 403, payAmt: 19478.47 }] // 业务数据
});
// 等同于 window.postMessage(data, '*')

页面上如果嵌入了第三方 iframe(广告、统计、社交插件),这些 iframe 的 message 事件监听器同样能收到这条消息

根因

postMessage 的第二个参数 targetOrigin 决定消息的接收范围:

targetOrigin行为
'*' 或省略广播到所有 frame,不检查来源
'https://example.com'只发送到 origin 为 https://example.com 的 frame
window.location.origin只发送到当前页面同源的 frame

省略第二个参数时,浏览器默认使用 '*'。这在 Chrome 扩展场景下尤其危险——扩展注入的脚本运行在电商平台页面,页面上可能有多个第三方 iframe。

解决方案

封装安全的 postMessage 工具函数

// safePostMessage:强制使用 window.location.origin
function safePostMessage(data) {
window.postMessage(data, window.location.origin);
}

// 使用
safePostMessage({
type: 'CCL_SHOP_REPORT_DAILY',
subType: 'daily',
memberId: memberId,
rows: [row]
});

接收端也要验证 origin

// content script 中监听消息
window.addEventListener('message', (event) => {
// ✅ 验证来源 origin
if (event.origin !== window.location.origin) return;

// ✅ 验证消息结构
if (!event.data || typeof event.data.type !== 'string') return;

switch (event.data.type) {
case 'CCL_SHOP_REPORT_DAILY':
handleDailyReport(event.data);
break;
case 'CCL_ITEM_WEEKLY_REPORT':
handleWeeklyReport(event.data);
break;
}
});

什么时候可以用 '*'

只有一种场景安全:消息内容完全不含敏感信息,且接收方 origin 不可预知。例如纯 UI 状态通知("面板已打开")。即使如此,用 window.location.origin 也更安全。

注意事项

注意事项

  • Chrome 扩展的 MAIN world 脚本和 content script 运行在不同的 JavaScript 隔离环境,postMessage 是它们通信的标准方式——务必保护好这条通道(遇到热重载后消息重复处理时需要手动清理旧监听器)
  • 接收端 event.origin 验证和发送端 targetOrigin 限定缺一不可,单向防护不完整
  • 如果消息需要跨 origin 传递(如从页面发到扩展 background),应使用 chrome.runtime.sendMessage(参考 Service Worker Token 同步)而不是 postMessage

Chrome 扩展采集数据全是空值?Cookie 同名不同值导致映射失败

· 阅读需 3 分钟

在为客户构建电商数据采集系统时遇到此问题,记录根因与解法。

TL;DR

1688 平台 Cookie 中 last_midunb 都包含用户 ID,但格式不同(b2b-xxx vs 纯数字)。数据库存的是 b2b- 前缀格式。原代码遍历 Cookie 数组时先匹配到了 unb,导致严格等于比较失败,所有采集数据写入时关联不到店铺,结果全是零值。

解法:把 Cookie key 优先级列表放在外层循环,Cookie 数组放在内层循环,确保高优先级的 key 先被查找。

问题现象

1688 生意参谋日报采集后,数据库里看板数据全是 0,但询盘数据有值:

shop_id | report_date | reveal_cnt | uv | pay_amt | effective_inq_users
2 | 2026-05-13 | 0 | 0 | 0.00 | 56
2 | 2026-05-14 | 0 | 0 | 0.00 | 41

后端日志报错:No shop found for memberId: 2214126315258

但数据库里存的 platform_account_idb2b-2214126315258ad300

根因

unb=2214126315258                          # 纯数字
last_mid=b2b-2214126315258ad300 # b2b- 前缀 + 后缀

数据库映射表 shops.platform_account_id 存的是 b2b- 前缀格式。代码做的是严格等于匹配:

const match = mapping.find(m => m.platform_account_id === memberId);
// "2214126315258" !== "b2b-2214126315258ad300" → 匹配失败

遍历顺序陷阱

原代码的外层循环是 Cookie 数组、内层是 key 列表:

// ❌ 错误:Cookie 数组在外层,key 优先级无效
var keys = ['last_mid', '__last_memberid__', 'unb'];
for (var i = 0; i < cookies.length; i++) { // 外层:Cookie
var pair = cookies[i].trim();
for (var k = 0; k < keys.length; k++) { // 内层:key
if (pair.indexOf(keys[k] + '=') === 0) {
return pair.substring(keys[k].length + 1);
}
}
}

document.cookie 的返回顺序不是固定的。如果 unb 的 Cookie 在数组中排在 last_mid 前面,就会先匹配到 unb,返回纯数字格式的 ID——last_mid 的优先级形同虚设。

解决方案

交换循环层级:key 优先级列表放在外层,Cookie 数组放在内层:

// ✅ 正确:key 优先级列表在外层
var keys = ['last_mid', '__last_memberid__', 'unb'];
for (var k = 0; k < keys.length; k++) { // 外层:按优先级遍历 key
for (var i = 0; i < cookies.length; i++) { // 内层:在所有 Cookie 中查找
var pair = cookies[i].trim();
if (pair.indexOf(keys[k] + '=') === 0) {
return pair.substring(keys[k].length + 1);
}
}
}

key 列表按优先级在外层遍历,无论 document.cookie 返回顺序如何,始终先查找 last_mid,保证拿到和数据库格式一致的用户 ID,映射不会失败。

验证

// 在浏览器控制台确认两个 Cookie 都存在
document.cookie.split(';')
.filter(c => /last_mid|unb/.test(c.trim()))
.map(c => c.trim())
// ['unb=2214126315258', 'last_mid=b2b-2214126315258ad300']

注意事项

Chrome 扩展中使用 chrome.cookies.get() 读取 Cookie 时不存在此问题——它是按名称精确查询,天然支持优先级。但 document.cookie 字符串解析时务必注意循环层级。另外,如果你的扩展通过 postMessage 传递数据,也要注意 postMessage targetOrigin 的安全风险;如果热重载后消息被处理两次,需要手动管理监听器生命周期。


Chrome 扩展热重载后消息被处理两次?WXT HMR 多实例叠加监听器

· 阅读需 3 分钟

在为客户构建电商数据采集 Chrome 扩展时遇到此问题,记录根因与解法。

TL;DR

WXT 框架 HMR 热重载时,content script 重新执行但旧的 window.addEventListener('message', ...) 不会被清除。每热重载一次就多一个监听器实例,导致每条 postMessage 被处理 N 次。

解法:注册新监听器前,从 window 变量取出旧监听器引用并 removeEventListener

问题现象

浏览器控制台显示同一条消息被两个不同实例捕获:

content.js:114 [CCL] CCL_SHOP_REPORT_DAILY daily caught - 实例: nxctn6
content.js:2 [CCL] CCL_SHOP_REPORT_DAILY daily caught - 实例: t6jce7

每条 postMessage 被处理两次,导致后台发送重复请求。

根因

WXT (基于 Vite 的 Chrome 扩展框架) 开发模式下,修改 content script 后触发 HMR:

  1. 新的 content script 模块被加载执行
  2. 新的 window.addEventListener('message', messageListener) 被注册
  3. 旧的监听器函数仍然存在于内存中——HMR 不负责清理 DOM 事件监听器

结果:window 上挂载了多个独立的 message 监听器,每条 postMessage 触发所有实例。

解决方案

在 content script 入口处,注册新监听器前移除旧的:

const instanceId = Math.random().toString(36).slice(2, 8);

// 取出旧监听器引用
const prevListener = (window as any).__cclMessageListener;
if (prevListener) {
window.removeEventListener('message', prevListener);
}

// 定义新监听器
const messageListener = (event: MessageEvent) => {
// ... 处理逻辑
};

// 存储当前引用(供下次 HMR 取用)
(window as any).__cclMessageListener = messageListener;

// 注册
window.addEventListener('message', messageListener);

关键点removeEventListener 必须传入和 addEventListener 相同的函数引用。把函数存到 window 变量上,下次 HMR 时就能取出旧引用并正确移除。这样无论热重载多少次,始终只有一个活跃的 message 监听器。

如果你同时遇到 postMessage 的 targetOrigin 安全问题Cookie 取值导致数据全零,建议一并排查。

注意事项

  • 此问题不仅限于 WXT,所有支持 content script 热重载的框架(Plasmo、CRXJS 等)都可能遇到
  • window 上的变量在页面刷新前一直存在,HMR 只替换脚本模块不清除 window 属性
  • 生产构建不存在此问题(content script 只加载一次),但开发时会产生难以排查的重复请求

Chrome 扩展 Service Worker 读不到登录态?跨上下文 Token 同步方案

· 阅读需 4 分钟

TL;DR

Chrome 扩展使用 sidepanel 作为 UI 入口,用户登录后 token 存入 localStorage。但 Service Worker(background script)没有 localStorage,调用直接抛 ReferenceError。解决方案:sidepanel 登录后通过 chrome.runtime.sendMessage 将 token 同步给 Service Worker,由 Service Worker 写入 chrome.storage.local。两边各取所需——sidepanel 读 localStorage,Service Worker 读 chrome.storage.local