手把手发起 XSS 攻击 + 如何防御
本文内容听译自 Youtube 视频:Running a XSS Attack + How to defend 并编写完善。
原作者:Maximilian Schwarzmüller
前言
如果你是一个 Web 开发者,那么你自然希望创建出安全的 Web 应用。跨站脚本攻击(Cross-Site Scripting Attack,简写为XSS)是现代 Web 应用中最大的威胁之一。因此很重要的一点就是你所编写的代码不易于被 XSS 攻击,而你也需要对什么是 XSS 攻击、在你的应用中哪里有潜在的安全漏洞有一个基本的了解。这就是在本期视频中我们将详细探索的内容。
这里有一个非常简易的 Web 应用代码,只有三个文件,而且只在浏览器里运行。想要发起 XSS 攻击的话还需要一个服务器,不过我也会展示如何基于此代码发起一个攻击,这也是我使用这个简易示例的原因。
我们可以发送以一些文本和一个图片构成的信息,这些信息会进而在下方依次简单地渲染出来。很容易想到,这个应用不可能只是在浏览器上运行,而是应该将文本和图片 URL 传输并储存到服务器的数据库里。没错,绝大多数网站就是这样做的。
比如一个用户可以购买商品,那么就会发起一个请求发送给服务器,来储存这一次购买信息。
再比如,有一个博客网站,你写了一篇博客并发表出去,那么就会被储存在数据库中,其它用户就能够访问到。
再比如,有一个公共论坛,用户可以相互交流,那么任何人发送消息,都会被存储到服务器数据库里,其它用户通过各种设备访问论坛的时候就会加载这些信息,从而展示在用户面前。
Web 应用的运作模式就是这样对吧。我们这里的这个应用也是这样,填写的信息也会被储存在数据库中。
发起攻击!
什么是 XSS 攻击
XSS 攻击指的是在我们用户使用的设备上执行恶意的 Javascript 代码。
简单的攻击策略
蹲点
我们首先通过页面审查,来到源代码标签,看到使整个网站运作起来的代码。
有的时候源代码会被压缩,从而很难读,不过最终依然可以读到。相关方法的介绍请查看我的其它视频。这里的代码其实就是 app.js
文件中的源码。
如下面代码所示,在用户提交时,输入的信息会进行校验,然后添加到 userMessages
数组中。
function formSubmitHandler(event) {
event.preventDefault();
const userMessageInput = event.target.querySelector('textarea');
const messageImageInput = event.target.querySelector('input');
const userMessage = userMessageInput.value;
const imageUrl = messageImageInput.value;
if (
!userMessage ||
!imageUrl ||
userMessage.trim().length === 0 ||
imageUrl.trim().length === 0
) {
alert('Please insert a valid message and image.');
return;
}
userMessages.push({
text: userMessage,
image: imageUrl,
});
userMessageInput.value = '';
messageImageInput.value = '';
renderMessages();
}
如果是现实环境,信息就会发送到服务器,存储到数据库,在页面加载时获取。
无论是现在这种傻乎乎的加入数组,还是从服务器获取,得到内容后,在下面这段代码里就会进行渲染。简单的渲染方式就是使用一个循环,并且最终构建出这样一串字符串,然后添加到 <ul>
元素的 innerHTML
中。
function renderMessages() {
let messageItems = '';
for (const message of userMessages) {
messageItems = `
${messageItems}
<li class="message-item">
<div class="message-image">
<img src="${message.image}" alt="${message.text}">
</div>
<p>${message.text}</p>
</li>
`;
}
userMessagesList.innerHTML = messageItems;
}
攻击
你能想到的其中一个简易的模式,应该是在 Your Message
一栏写入 <script>
标签,然后在其中写一些恶意代码。
在这个例子中,我就只写了一行 alert
,弹出“你被黑了!”。
正因为使用了 innerHTML
的缘故,我们加入的 <script>
标签应该会被转译成 HTML 然后被执行吧?
没起作用?
尝试之后发现,并没有什么效果。但是如果这个时候再去页面元素中看,我们会发现 <script>
标签已经被渲染出来了, 如下图所示。
没有生效的原因是,现代浏览器知道这样一种攻击模式漏洞,并且已经做出了防御。防御手段就是任何加入到 innerHTML
中的代码都不会被执行。
不过我下面将展示一种能够成功让代码得到执行的方式。
代码成功注入的后果
我们都知道如果注入的代码成功被执行将会带来什么后果。这里的示例是一个纯前端的东西,不会往服务器中存储,所以我们只能自己黑自己。一旦存储到了服务器里,其它用户就也会得到执行注入代码的机会,这样一来就能把坏事做尽。
我们能够偷取到 Local Storage
或者 Cookies
中的内容;
我们能够把偷来的数据存储到自己的服务器上,
或者伪造他人身份来发送请求,在他人不知情的情况下替代他人买东西。
这些都是潜在的威胁。
换一个攻击策略
这一次我们在 Your Message
一栏中输入普通的内容,但是 Message Image
这一栏呢?我们原本需要填入的是图片链接,而且这个链接同样也会通过 innerHTML
被渲染到 <img>
标签里面,也就是之前看到的这一行字符串代码:
`<img src="${message.image}" alt="${message.text}">`
如果我们能够改变这一行字符串最终形成的内容,把这一整个 <img>
元素都换掉呢?
改造图片链接
首先,我们先填入一个无效的图片链接,比如:
some-page.com/no-image.jpg
接下来,在后面加上一个双引号 "
,这样就能够把 <img>
标签中的 src
属性闭合。实际渲染出来的结果就会变成:
<img src="some-page.com/no-image.jpg" /*剩余内容*/ alt="${message.text}">
对于 <img>
标签来说,有一个独特的属性是 onerror
,这个属性也是被官方所支持的。onerror
中需要的是 Javascript 代码,当获取图片失败的时候将被调用。因此我们提供了一个无效链接后,就能强制执行 onerror
中的内容,不需要 <script>
标签了。因此改造完成后,我们填入的内容就是:
some-page.com/no-image.jpg“ onerror="alert("你被黑了!")"
现在页面上就真的弹出了提示框,这就是一次成功的 XSS 攻击了。
有必要重申一遍,在这个示例中,我们只能对自己黑着玩,但是这样的数据是能够存储在数据库中的,也会有机会在其它用户的设备上执行。
这就是一个简单的 XSS 攻击。在任何网站中找到这样脆弱的攻击点并不难,而且一瞬间就能让网站出现巨大的问题。
发起防御!
那么作为网站的开发者,如何才能在开发 Web 应用时建起围墙呢?有许多方法。
首先,你自然不能采取上面构建 innerHTML 的方式把用户输入的内容直接塞到一个标签里。
不过还有更为重要的步骤需要做。
清理用户的脏输入
如果你有着使用 Node.js 编写后端代码的经验,那么你可以搜索一下 node sanitize
,类似的包在 php 等语言上都有。
清理的意思是,用户输入的内容会通过某种模式进行检查,然后将其中的恶意部分移除,这样最终储存的内容就是“干净”的。
这样的清理包无论是在浏览器还是在服务器中都是推荐使用的,简言之,在储存用户输入的数据之前,有这么一个流程能够将用户输入的内容进行清理是十分重要的。
使用框架
除了清洁输入以外,在客户端也同样可以进行一层保护。如果你使用的是类似 React,Angular 或者 Vue 的框架或库,这些框架也已经在内部集成了 HTML 的转译。
但是这并不意味着使用了框架就不需要再在后端进行输入清理。这样为整体应用的安全性能多加了一层保护措施。
第三方库攻击
除此之外,还存在着一类攻击途径,这一类更具有技巧性。我们这里使用的是一个使用原生 Javascript 的应用,但是在现实生活中我们都知道,一个大型项目是由数百个不同的包、数百个第三方库构成的,比如 react
,react-router
, 组件库等。
我们需要知道的是,所有这些库都会在整个项目中加入新的会被执行的 Javascript 代码。如果某一个库或者框架中就存在着恶意代码,这样以来就能够正常执行,也不会被清理。
换句话说,你使用了一个被认可使用,却具有恶意代码的第三方库,那么整个项目的风险系数就很高了。
为此,npm 的审计(audit)功能能够让你对自己使用的第三方库进行已知薄弱点审查。那么对于未知的其它薄弱点,依然存在着风险。
庆幸如今有很多开源的项目,我们可以进入代码中来看到哪些内容将会在我们自己的项目中执行,不过实际上我们并不会特地这样做。
对于 Angular
这样的库,我们能够放心使用,因为 Google 还没对攻击我们这些开发者有什么计划。对于小型一些的库来说,如果存在着恶意代码,有可能并不是因为这些库的作者是个坏蛋,而有可能是因为他们引入了其它地方的代码,从而进一步在不知情的情况下引入了恶意代码。
综上,第三方库也是攻击来源之一,但这么说的意思并不是不让你用,而是在使用的时候要有意识地做到防范。使用前请三思,也许你并不是一定需要引入某个库来做具有动画效果的提示文本,也许你可以缩减所使用到的第三方库的数量,这样对于用户的压力也更小一些,还能给页面提速。
小结
如今,大型且流行的库和框架自然是很安全的,但是依然是无法达到 100% 的安全。我们自身依然需要对于先前提到过的几类攻击模式进行防御,通过清理用户输入和提高潜在风险的安全意识。
以上就是一个最为基础和简易的 XSS 攻击示例以及一个常见的薄弱漏洞。
我的频道里有着更多关于 Local Storage
、Cookie
、身份验证、数据存储相关的视频,希望这些能够帮助你构建出一个具有更高安全性的 Web 应用。