当吾辈遇上 Firefox 中 9 年的陈年老 Bug

本文最后更新于:2025年2月18日 上午

背景

最近开发一个跨浏览器的扩展时,由于需要在 Content Script 中请求远端服务 API,在 Chrome 中没有遇到任何问题,但在 Firefox 中,它会在 Content Script 上应用网站的 CSP 规则。不幸的是,一些网站禁止在 Script 中请求它们不知道的 API 端点。

首先,这里出现了一个关键名词:CSP,又叫内容安全策略,主要是用来解决 XSS 的。基本上,它允许网站所有者通过服务端响应 Content-Security-Policy Header 来控制网站页面可以请求的 API 端点。例如下面这个规则仅允许请求 https://onlinebanking.jumbobank.com,其他 API 端点的请求都将被浏览器拒绝。

1
Content-Security-Policy: default-src https://onlinebanking.jumbobank.com

也就是说,你可以打开 Twitter,并在网站 Devtools Console 中执行 fetch('https://placehold.co/200'),然后就得到了一个 CSP 错误。

1739867422408.jpg

如果你将相同的代码放在扩展的 Content Script 中,然后在 Chrome 中测试扩展,一切正常。

1739869145008.jpg

而在 Firefox 中,嗯,你仍然会得到一个 CSP 错误。

1739869071702.jpg

如果你使用 Manifest V2,Firefox 则会正常放过,并且不会显示在 Network 中。吾辈甚至不想知道它做了什么。

1739869022575.jpg

经过一番调查,吾辈成功找到了相关的 issue,而它们均创建于 9 年前,最新的讨论甚至在 4 天前。检查下面两个 issue。

思路

那么,问题就在这儿,看起来也无法在短期内解决,如果想要让自己的扩展支持 Firefox,现在应该怎么办?
好吧,基本思路有 2 个:

  1. 绕过去,如果 Content Script 无法不能请求,为什么不放在 Background Script 中然后加一层转发呢?
  2. 如果网站的 CSP 有麻烦,为什么不使用 declarativeNetRequest/webRequestBlocking API 来修改或删除它们呢?

Content Script => Background Script 转发

首先需要在 Background Script 中定义接口,准备接受参数并转发请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/entrypoints/background.ts
browser.runtime.onMessage.addListener(
(
message: {
type: 'fetch'
request: {
url: string
}
},
_sender,
sendResponse,
) => {
if (message.type === 'fetch') {
// 这儿看起来非常“怪异”,但没办法,Chrome 就是这样定义的
fetch(message.request.url)
.then((r) => r.blob())
.then((b) => sendResponse(b))
return true
}
},
)

同时必须在 manifest 中 声明正确的 host_permissions 权限,添加你要请求的域名。

1
2
3
{
"host_permissions": ["https://placehold.co/**"]
}

然后在 Content Script 中调用它。

1
2
3
4
5
6
7
8
9
10
// src/entrypoints/content.ts
console.log(
'Hello content.',
await browser.runtime.sendMessage({
type: 'fetch',
request: {
url: 'https://placehold.co/200',
},
}),
)

可以看到现在可以正常得到结果了,但这种方式的主要问题是与原始的 fetch 接口并不相同,上面实现了 blob 类型的请求接口,但并未完整支持 fetch 的所有功能。嗯,考虑到 Request/Response 都不是完全可以序列化的,这会有点麻烦。

1739871065758.jpg

使用 declarativeNetRequest API 来删除网站的 CSP 设置

接下来,将介绍一种非侵入式的方法,允许在不修改 Content Script 解决该问题,首先是 declarativeNetRequest API,由于 WebRequestBlocking API 被广泛的应用于广告拦截器中,直接导致了 Chrome Manifest V3 正式将其废弃,并推出了静态的 declarativeNetRequest API 替代(尽管远远不能完全替代),但对于解决当下的这个问题很有用,而且很简单

首先在 manifest 中声明权限,注意 host_permissions 需要包含你要处理的网站。

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"host_permissions": ["https://x.com/**"],
"permissions": ["declarativeNetRequest"],
"declarative_net_request": {
"rule_resources": [
{
"id": "ruleset",
"enabled": true,
"path": "rules.json"
}
]
}
}

然后在 public 目录中添加一个 rules.json 文件,其中定义了要删除 x.com 上的 content-security-policy response header。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[
{
"id": 1,
"condition": {
"urlFilter": "https://x.com/**",
"excludedResourceTypes": []
},
"action": {
"type": "modifyHeaders",
"responseHeaders": [
{
"header": "content-security-policy",
"operation": "remove"
}
]
}
}
]

可以看到网站的 CSP 已经不复存在,可以看到浏览器也不会拦截你的请求了。但是,这种方法的主要问题是网站安全性受损,就这点而言,这不是一个好方法。

1739874302081.jpg
1739874464160.jpg

参考

一般而言,推荐使用 Background Script 转发请求,尽管它要编写更多的样板代码,吾辈也就此在 WXT 上问过框架作者,他似乎一般也会这样做。

参考: https://github.com/wxt-dev/wxt/discussions/1442#discussioncomment-12219769


当吾辈遇上 Firefox 中 9 年的陈年老 Bug
https://blog.rxliuli.com/p/23bc670bb5e54e6190d3ecf39cc3efd4/
作者
rxliuli
发布于
2025年2月16日
许可协议