本文引用部分内容引用自# 前端安全系列(一):如何防止XSS攻击?
XSS攻击介绍
跨站脚本攻击(Cross-site scripting,XSS)是一种安全漏洞,攻击者可以利用这种漏洞在网站上注入恶意的客户端代码。若受害者运行这些恶意代码,攻击者就可以突破网站的访问限制并冒充受害者。根据开放式 Web 应用安全项目(OWASP),XSS 在 2017 年被认为 7 种最常见的 Web 应用程序漏洞之一。
XSS 攻击可以分为 3 类:存储型(持久型)、反射型(非持久型)、DOM 型。
-
存储型 XSS
注入型脚本永久存储在目标服务器上。当浏览器请求数据时,脚本从服务器上传回并执行。
-
反射型 XSS
当用户点击一个恶意链接,或者提交一个表单,或者进入一个恶意网站时,注入脚本进入被攻击者的网站。Web 服务器将注入脚本,比如一个错误信息,搜索结果等 返回到用户的浏览器上。由于浏览器认为这个响应来自"可信任"的服务器,所以会执行这段脚本。
-
基于 DOM 的 XSS
通过修改原始的客户端代码,受害者浏览器的 DOM 环境改变,导致有效载荷的执行。也就是说,页面本身并没有变化,但由于 DOM 环境被恶意修改,有客户端代码被包含进了页面,并且意外执行。
XSS常见注入方法
-
在 HTML 中内嵌的文本中,恶意内容以 script 标签形成注入。
-
在内联的 JavaScript 中,拼接的数据突破了原本的限制(字符串,变量,方法名等)。
-
在标签属性中,恶意内容包含引号,从而突破属性值的限制,注入其他属性或者标签。
-
在标签的 href、src 等属性中,包含
javascript:
(伪协议)等可执行代码。 -
在 onload、onerror、onclick 等事件中,注入不受控制代码。
-
在 style 属性和标签中,包含类似
background-image:url("javascript:...");
的代码(新版本浏览器已经可以防范)。 -
在 style 属性和标签中,包含类似
expression(...)
的 CSS 表达式代码(新版本浏览器已经可以防范)。
反射型XSS
恶意 JavaScript
脚本属于用户发送给网站请求中的一部分,随后网站又将这部分返回给用户,恶意脚本在页面中被执行。一般发生在前后端一体的应用中,服务端逻辑会改变最终的网页代码。
简单概括,就是前端发送请求链接,后端将请求中的参数拼接到HTML中返回。而XSS攻击使得逻辑跳出了预先准备好的框架,使其以不符合预期的行为运行
攻击步骤
- 攻击者构造出特殊的 URL,其中包含恶意代码。
- 用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。
- 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
存储型XSS
黑客将恶意 JavaScript
脚本长期保存在服务端数据库中,用户一旦访问相关页面数据,恶意脚本就会被执行。常见于搜索、微博、社区贴吧评论等。
存储型 XSS 的攻击步骤:
- 攻击者将恶意代码提交到目标网站的数据库中。
- 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器。
- 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
DOM型XSS攻击
DOM 型 XSS 攻击,实际上就是网站前端 JavaScript 代码本身不够严谨,把不可信的数据当作代码执行了。
在使用 .innerHTML
、.outerHTML
、document.write()
时要特别小心,不要把不可信的数据作为 HTML 插到页面上,而应尽量使用 .textContent
、.setAttribute()
等
DOM 型 XSS 攻击,实际上就是网站前端 JavaScript 代码本身不够严谨,把不可信的数据当作代码执行了。
在使用 .innerHTML
、.outerHTML
、document.write()
时要特别小心,不要把不可信的数据作为 HTML 插到页面上,而应尽量使用 .textContent
、.setAttribute()
等
DOM型XSS的攻击步骤
- 攻击者构造出特殊的 URL,其中包含恶意代码。
- 用户打开带有恶意代码的 URL。
- 用户浏览器接收到响应后解析执行,前端 JavaScript 取出 URL 中的恶意代码并执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
攻击案例
script标签注入
案例
html
<input type="text" value="getParameter("keyword")">
<button>搜索</button>
<div>
您搜索的关键词是: {{getParameter("keyword")}}
</div>
倘若此时有一个请求为:
http://xxx/search?keyword=">script>alert('XSS');</script>
点击该请求,会发现代码将">script>alert('XSS');</script>
拼接到了html中,此时html为,运行后浏览器将会执行script
内代码
html
<input type="text" value=""><script>alert('XSS');</script>">
<button>搜索</button>
<div>
您搜索的关键词是:"><script>alert('XSS');</script>
</div>
解决方案
此时我们可以通过转义html关键字符来避免script标签插入,例如利用escapeHTML()
字符 | 转义后的字符 |
---|---|
& | & |
< | < |
> | > |
" | " |
' | ' |
/ | / |
注意,尽量使用业界成熟的转义方案
HTML属性更改,特殊JavaScript API
案例
html
<a href="escapeHTML(getParameter('redirect_to'))">跳转...</a>
倘若此时有一个请求:
http://xxx/?redirect_to=javascript:alert('XSS')
此时url参数redirect_to
会被拼接到a标签的href属性中,当用户点击改标签时,就会触发javascript脚本
html
<a href="javascript:alert('XSS')">跳转...</a>
解决方案
遇到这种类似的情景,必须要对用户的输入进行校验,禁止以javascript:
开头的链接,或其他非法的scheme
XSS攻击的预防
根据上文的介绍,我们可以讲XSS攻击分成两部分
- 攻击者提交恶意代码
- 浏览器执行恶意代码
根据这两个部分我们可以进行针对性的预防
输入过滤
在进行输入过滤时,时机是一个值得思考的问题,如果由前端执行,那么很有可能攻击者会直接绕过前端过滤,直接构造请求,这样就可以提交代码了。
那如果是后端过滤呢?后端在写入数据库前,对输入进行过滤,然后把内容返回给前端。但需要考虑的问题是:前端何时进行转义
- 后端存储的内容也可能会发往客户端,而经过HTML的转义,客户端无法正确显示字符
- 即使是前端,在不同的地方所需的编码也不同
因此,输入过滤很多情况只能用于明确的输入类型,例如数字,URL,电话号码,邮件地址等等内容
预防存储型和反射型XSS攻击
让我们回忆一下存储型和反射型XSS攻击,这两个攻击都是攻击者通过将恶意代码提交到服务器,由服务器存储或拼接html后返回结果,而这个结果就包含了攻击者恶意写入的内容
预防这两种漏洞,通常有两种常见的做法:
- 改成纯前端渲染,将代码和数据分隔
- 对HTML进行充分的转义
纯前端渲染
纯前端渲染的过程:
- 浏览器先加载一个静态 HTML,此 HTML 中不包含任何跟业务相关的数据。
- 然后浏览器执行 HTML 中的 JavaScript。
- JavaScript 通过 Ajax 加载业务数据,调用 DOM API 更新到页面上。
在纯前端渲染中,我们会明确的告诉浏览器:下面要设置的内容是文本(.innerText
),还是属性(.setAttribute
),还是样式(.style
)等等。浏览器不会被轻易的被欺骗,执行预期外的代码了。
但纯前端渲染还需注意避免 DOM 型 XSS 漏洞(例如 onload
事件和 href
中的 javascript:xxx
等,请参考下文"预防 DOM 型 XSS 攻击"部分)。
在很多内部、管理系统中,采用纯前端渲染是非常合适的。但对于性能要求高,或有 SEO 需求的页面,我们仍然要面对拼接 HTML 的问题。
转义HTML
很多时候我们无法避免拼接HTML,此时我们就需要严格的转义规则去避免浏览器执行额外的内容。
通常我们会采用一些合适的转义库,从而对HTML模版各处进行充分的转义
预防DOM型XSS攻击
DOM 型 XSS 攻击,实际上就是网站前端 JavaScript 代码本身不够严谨,把不可信的数据当作代码执行了。
在使用 .innerHTML
、.outerHTML
、document.write()
时要特别小心,不要把不可信的数据作为 HTML 插到页面上,而应尽量使用 .textContent
、.setAttribute()
等。
如果用 Vue/React 技术栈,并且不使用 v-html
/dangerouslySetInnerHTML
功能,就在前端 render 阶段避免 innerHTML
、outerHTML
的 XSS 隐患。
DOM 中的内联事件监听器,如 location
、onclick
、onerror
、onload
、onmouseover
等,<a>
标签的 href
属性,JavaScript 的 eval()
、setTimeout()
、setInterval()
等,都能把字符串作为代码运行。如果不可信的数据拼接到字符串中传递给这些 API,很容易产生安全隐患,请务必避免。
使用Content Security Policy
内容安全策略 (CSP)是一个额外的安全层,用于检测并削弱某些特定类型的攻击,包括跨站脚本(XSS)和数据注入攻击等。无论是数据盗取、网站内容污染还是恶意软件分发,这些攻击都是主要的手段。
启用
配置网络服务器返回 Content-Securiry-Policy
HTTP标头
此外,<meta>
元素也可以被用来配置改策略
html
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; img-src https://*; child-src 'none';" />
作用
严格的CSP在XSS的防范中可以起到以下作用
- 禁止加载外域代码,防止复杂的攻击逻辑。
- 禁止外域提交,网站被攻击后,用户的数据不会泄露到外域。
- 禁止内联脚本执行(规则较严格,目前发现 GitHub 使用)。
- 禁止未授权的脚本执行(新特性,Google Map 移动版在使用)。
- 合理使用上报可以及时发现 XSS,利于尽快修复问题。
输入内容长度控制
对于不受信任的输入,都应该限定一个合理的长度。虽然无法完全防止 XSS 发生,但可以增加 XSS 攻击的难度。
其他安全措施
- HTTP-only Cookie: 禁止 JavaScript 读取某些敏感 Cookie,攻击者完成 XSS 注入后也无法窃取此 Cookie。
- 验证码:防止脚本冒充用户提交危险操作。