2023 年 11 月 9 日,Sentry 在其博客上发布了一篇题为Next.js SDK 安全建议 - CVE-2023-46729 的文章。文章讨论了CVE-2023-46729漏洞的详细信息,包括其原因、发现时间和修补时间。
虽然该漏洞于11/9正式公布,但实际上已在10/31发布的7.77.0版本中修复。给开发人员一些时间来修补该漏洞。
下面我们简单讨论一下这个漏洞的成因和攻击方法。
漏洞分析
GitHub 上还有更多技术说明:CVE-2023-46729:SSRF via Next.js SDK 隧道端点:
https://github.com/getsentry/sentry-javascript/security/advisories/GHSA-2rmr-xw8m-22q9
你可以看这一段:
Next.js SDK 隧道端点的未经净化的输入允许将 HTTP 请求发送到任意 URL 并将响应反射回用户。
在Sentry中,有一个称为“隧道”的功能,官方文档中的这张图片完美地解释了为什么需要隧道:
如果没有隧道,发送到 Sentry 的请求将直接通过前端的浏览器发送。但是,这些直接发送到 Sentry 的请求可能会被广告拦截器拦截,从而导致 Sentry 无法接收数据。如果启用了隧道,请求首先发送到用户自己的服务器,然后转发到Sentry。这样,该请求就成为同源请求,不会被广告拦截器拦截。
在专门为 Next.js 设计的 Sentry SDK 中,使用了一个称为重写的功能。这是官方文档中的一个示例:
module.exports = {
async rewrites() {
return [
{
source: '/blog',
destination: 'https://example.com/blog',
},
{
source: '/blog/:slug',
destination: 'https://example.com/blog/:slug', // Matched parameters can be used in the destination
},
]
},
}
Next.js重写可以分为两种:内部重写和外部重写。后者更像是一个代理,因为它可以直接将请求重定向到外部网站并显示响应。
Next.js Sentry SDK 的实现位于sentry-javascript/packages/nextjs/src/config/withSentryConfig.ts中:
/**
* Injects rewrite rules into the Next.js config provided by the user to tunnel
* requests from the `tunnelPath` to Sentry.
*
* See https://nextjs.org/docs/api-reference/next.config.js/rewrites.
*/
function setUpTunnelRewriteRules(userNextConfig: NextConfigObject, tunnelPath: string): void {
const originalRewrites = userNextConfig.rewrites;
// This function doesn't take any arguments at the time of writing but we future-proof
// here in case Next.js ever decides to pass some
userNextConfig.rewrites = async (...args: unknown[]) => {
const injectedRewrite = {
// Matched rewrite routes will look like the following: `[tunnelPath]?o=[orgid]&p=[projectid]`
// Nextjs will automatically convert `source` into a regex for us
source: `${tunnelPath}(/?)`,
has: [
{
type: 'query',
key: 'o', // short for orgId - we keep it short so matching is harder for ad-blockers
value: '(?<orgid>.*)',
},
{
type: 'query',
key: 'p', // short for projectId - we keep it short so matching is harder for ad-blockers
value: '(?<projectid>.*)',
},
],
destination: 'https://o:orgid.ingest.sentry.io/api/:projectid/envelope/?hsts=0',
};
if (typeof originalRewrites !== 'function') {
return [injectedRewrite];
}
// @ts-expect-error Expected 0 arguments but got 1 - this is from the future-proofing mentioned above, so we don't care about it
const originalRewritesResult = await originalRewrites(...args);
if (Array.isArray(originalRewritesResult)) {
return [injectedRewrite, ...originalRewritesResult];
} else {
return {
...originalRewritesResult,
beforeFiles: [injectedRewrite, ...(originalRewritesResult.beforeFiles || [])],
};
}
};
}
关键部分是这一部分:
const injectedRewrite = {
// Matched rewrite routes will look like the following: `[tunnelPath]?o=[orgid]&p=[projectid]`
// Nextjs will automatically convert `source` into a regex for us
source: `${tunnelPath}(/?)`,
has: [
{
type: 'query',
key: 'o', // short for orgId - we keep it short so matching is harder for ad-blockers
value: '(?<orgid>.*)',
},
{
type: 'query',
key: 'p', // short for projectId - we keep it short so matching is harder for ad-blockers
value: '(?<projectid>.*)',
},
],
destination: 'https://o:orgid.ingest.sentry.io/api/:projectid/envelope/?hsts=0',
};
它根据o和p查询字符串参数确定要重定向到的最终 URL。
这里的问题是这两个参数都使用.*正则表达式,它匹配任何字符。换句话说,对于以下 URL:
https://huli.tw/tunnel?o=abc&p=def
它将代理:
https://oabc.ingest.sentry.io/api/def/envelope/?hsts=0
看起来不错,但是如果是这样呢?
https://huli.tw/tunnel?o=example.com%23&p=def
%23是 的 URL 编码结果#。它将被代理至:
https://oexample.com#.ingest.sentry.io/api/def/envelope/?hsts=0
我们使用#将原始主机名作为哈希的一部分包含在内,并成功更改代理的目的地。然而,领先o有点烦人。@让我们通过在开头添加来摆脱它:
https://huli.tw/[email protected]%23&p=def
它成为了:
https://[email protected]#.ingest.sentry.io/api/def/envelope/?hsts=0
这样,攻击者就可以使用该o参数来更改代理的目的地,并将请求重定向到任何地方。如前所述,此重写功能直接返回响应。因此,当用户访问时https://huli.tw/tunnel?o=@example.com%23&p=def,他们会看到 的响应example.com。
换句话说,如果攻击者将请求重定向到自己的网站,他们就可以输出<script>alert(document.cookie)</script>,从而将其变成 XSS 漏洞。
如果攻击者将请求重定向到类似 的其他内部网页https://localhost:3001,则成为 SSRF 漏洞(但目标必须支持 HTTPS)。
至于修复,很简单。只需向正则表达式添加一些限制即可。最后,Sentry 将其调整为仅允许数字:
{
type: 'query',
key: 'o', // short for orgId - we keep it short so matching is harder for ad-blockers
value: '(?<orgid>\\d*)',
},
此问题已在 7.77.0 及更高版本中修复。
结论
该漏洞非常简单且易于重现。只需找到修复提交并查看代码即可了解如何利用它。
总而言之,在进行 URL 重写时,您确实需要谨慎,因为很容易遇到问题(尤其是当您不仅重写路径而是整个 URL 时)。
感谢您抽出
.
.
来阅读本文
点它,分享点赞在看都在这里