浏览器插件的核心能力往往依赖内容脚本和背景页的协同工作,内容脚本运行在网页的上下文环境中,负责和页面DOM交互、获取页面数据,背景页则运行在独立的插件进程里,负责处理持久化逻辑、跨域请求、权限校验等核心任务。两者的通信链路如果设计不当,就可能成为插件的安全短板。

内容脚本与背景页通信的常见风险场景
很多开发者在开发插件时,会直接使用浏览器提供的chrome.runtime.sendMessage方法实现两者通信,却没有做任何安全校验。这种无防护的通信方式会引入多个安全风险,最常见的就是消息来源不可信的问题。内容脚本运行在网页的上下文中,恶意网页可以通过注入脚本的方式,模拟内容脚本向背景页发送消息,如果背景页没有校验消息的发送方身份,就可能执行恶意指令,比如读取插件的存储数据、触发跨域请求等。
另一个常见风险是数据篡改和注入攻击。如果通信过程中传递的是未经过滤的用户输入数据,恶意内容脚本可以构造特殊的消息内容,注入恶意代码到背景页的执行逻辑中。比如背景页收到消息后直接把消息内容拼接到DOM操作或者eval执行的逻辑里,就会触发XSS类的安全问题。还有部分开发者会把敏感信息比如用户token、接口密钥直接通过消息传递,这些内容如果被中间层拦截或者日志泄露,会造成严重的敏感信息泄露。
权限越界也是需要重点关注的问题。内容脚本本身只有有限的权限,正常情况下不应该触发需要插件高级权限的操作,比如修改浏览器设置、访问所有标签页的数据。如果背景页没有对消息请求的权限做校验,内容脚本就可以绕过自身的权限限制,通过消息让背景页执行高权限操作,相当于把插件的全部权限都暴露给了运行内容脚本的网页,大大提升了攻击面。
安全通信协议的核心设计原则
设计安全通信协议首先要明确消息来源的可信校验机制。浏览器提供了sender对象来标识消息发送方的身份,在背景页接收消息时,可以通过sender.tab和sender.url判断消息是否来自插件自己的内容脚本。sender.tab可以拿到发送消息的标签页信息,sender.url可以确认发送方的URL是否符合插件的预期,比如内容脚本的URL应该是chrome-extension://插件ID/xxx.js的格式,只要不符合这个格式的消息都可以直接拒绝。
其次要规范消息的数据格式和校验规则。建议统一使用结构化的消息格式,比如约定消息必须包含type(消息类型)、data(消息内容)、timestamp(时间戳)、sign(签名)四个字段。其中type用来标识消息的用途,背景页可以根据type做对应的权限校验,比如只有type为get_public_data的消息才允许被处理,高权限的type需要额外校验。data字段的内容需要做严格的类型校验和过滤,避免传递未经过处理的用户输入。
时间戳和签名机制可以防止消息重放攻击。每次发送消息时带上当前的时间戳,背景页收到消息后校验时间戳是否在合理的时间窗口内,比如5秒内的消息才有效,超过时间的消息直接丢弃。签名则可以用插件预定义的密钥对消息内容、时间戳做哈希计算,背景页收到消息后重新计算签名,和消息携带的签名对比,不一致的消息说明被篡改过,直接拒绝处理。这样即使消息被拦截,攻击者也无法篡改内容或者重放旧消息。
安全通信协议的具体实现示例
首先看内容脚本侧的安全发送实现,内容脚本在发送消息前需要先生成时间戳和签名,签名可以用简单的HMAC算法,这里为了示例简化,用预定义的密钥做拼接哈希。内容脚本需要先获取插件的ID和预定义的通信密钥,这些密钥可以存在插件的存储里,避免硬编码在代码中。
// 内容脚本发送消息的安全实现
const PLUGIN_ID = chrome.runtime.id;
const SECRET_KEY = 'your_secure_secret_key_here'; // 实际项目中可以存在chrome.storage里
function sendSecureMessage(type, data) {
const timestamp = Date.now();
// 构造消息体
const message = {
type: type,
data: data,
timestamp: timestamp,
sender: PLUGIN_ID
};
// 生成签名:对type、data、timestamp、sender拼接后做哈希
const signStr = `${type}${JSON.stringify(data)}${timestamp}${PLUGIN_ID}${SECRET_KEY}`;
const sign = btoa(unescape(encodeURIComponent(signStr))); // 简单哈希示例,实际可用更安全的算法
message.sign = sign;
// 发送消息
chrome.runtime.sendMessage(message, (response) => {
if (chrome.runtime.lastError) {
console.error('消息发送失败:', chrome.runtime.lastError);
return;
}
console.log('收到背景页响应:', response);
});
}
// 调用示例:请求获取公开数据
sendSecureMessage('get_public_data', { pageUrl: window.location.href });
接下来是背景页侧的安全接收和处理实现,背景页需要监听消息,先做来源校验、签名校验、时间戳校验,再做权限校验,最后处理消息返回响应。背景页首先要校验sender的身份,确保消息来自插件自己的内容脚本,避免第三方网页发来的消息被处理。
// 背景页接收消息的安全实现
const SECRET_KEY = 'your_secure_secret_key_here'; // 和内容脚本的密钥一致
const VALID_TIME_WINDOW = 5000; // 5秒有效期
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
// 1. 校验消息来源
if (!sender.url || !sender.url.startsWith(`chrome-extension://${chrome.runtime.id}/`)) {
console.warn('拒绝非插件来源的消息:', sender.url);
return false;
}
// 2. 校验消息必要字段
if (!message.type || !message.timestamp || !message.sign || !message.sender) {
console.warn('消息字段不完整,拒绝处理');
return false;
}
// 3. 校验时间戳,防止重放攻击
const now = Date.now();
if (Math.abs(now - message.timestamp) > VALID_TIME_WINDOW) {
console.warn('消息时间戳过期,拒绝处理');
return false;
}
// 4. 校验签名,防止消息篡改
const signStr = `${message.type}${JSON.stringify(message.data)}${message.timestamp}${message.sender}${SECRET_KEY}`;
const expectedSign = btoa(unescape(encodeURIComponent(signStr)));
if (expectedSign !== message.sign) {
console.warn('消息签名校验失败,可能被篡改');
return false;
}
// 5. 根据消息类型做权限校验和处理
switch (message.type) {
case 'get_public_data':
// 公开数据不需要额外权限,直接处理
const publicData = { version: '1.0.0', support: true };
sendResponse({ success: true, data: publicData });
break;
case 'get_user_token':
// 敏感操作需要额外校验,这里示例直接拒绝内容脚本的该请求
console.warn('内容脚本无权请求用户token');
sendResponse({ success: false, error: '无权限' });
break;
default:
sendResponse({ success: false, error: '未知消息类型' });
}
return true; // 保持消息通道开启,等待异步响应
});
除了上述的点对点消息传递,还可以使用长连接chrome.runtime.connect实现更稳定的通信,长连接的安全校验逻辑和单次消息一致,只是需要在连接建立时做一次身份校验,后续的消息可以复用连接,减少重复校验的开销。长连接适合需要频繁通信的场景,比如内容脚本需要实时同步页面状态到背景页,这时候用长连接比多次发送单次消息效率更高,同时也能保持同样的安全防护能力。
不同通信方式的安全对比与最佳实践
浏览器插件提供了两种主要的通信方式,一种是单次消息传递sendMessage,另一种是长连接connect。单次消息适合低频、一次性的通信场景,比如内容脚本请求一次背景页的数据,用完即走,实现简单,但是每次通信都需要做完整的校验,开销相对大一点。长连接适合高频通信场景,连接建立后只需要一次校验,后续消息可以直接传递,效率更高,但是需要额外处理连接断开、重连的逻辑,避免连接被恶意利用。
从安全角度来看,两种方式的核心防护逻辑是一致的,都需要做来源校验、签名校验、权限控制。但是长连接需要注意连接的生命周期管理,比如内容脚本所在的标签页关闭时,要及时断开长连接,避免连接被残留的恶意脚本利用。另外,不管用哪种通信方式,都不要在消息中传递敏感信息,比如用户密码、接口密钥等,如果必须传递敏感数据,需要额外做加密处理,比如用非对称加密,内容脚本用公钥加密数据,背景页用私钥解密,避免数据在传输过程中被泄露。
最后的最佳实践建议:第一,所有通信逻辑都封装成统一的工具函数,不要在每个通信的地方重复写校验逻辑,避免遗漏校验步骤;第二,定期更新通信密钥,避免密钥泄露后长期被利用;第三,在插件的权限声明里最小化处理,只申请必要的权限,减少背景页被攻击后的影响范围;第四,对所有的消息处理做日志记录,方便后续排查安全问题,但是日志中不要记录敏感信息。通过这些措施,可以最大程度保证内容脚本和背景页的通信安全,降低插件被攻击的风险。
content_scriptbackground_page浏览器插件安全修改时间:2026-08-16 09:21:09