跨站脚本攻击(XSS)学习记录

99 阅读17分钟

问题:为什么Cookie中有HttpOnly属性?

浏览器一般默认页面中可以引用任意第三方资源,然后又引入 CSP 策略来加以限制; 默认 XMLHttpRequest 和 Fetch 不能跨站请求资源,然后又通过 CORS 策略来支持其跨域。

支持页面中的第三方资源引用和 CORS 也带来了很多安全问题,其中最典型的就是 XSS 攻击。

XSS 攻击

XSS 全称是 Cross Site Scripting。为了与“CSS”区分开来,故简称 XSS。 “跨站脚本”。XSS 攻击是指黑客往 HTML 文件中或者 DOM 中注入恶意脚本,从而在用户浏览页面时利用注入的恶意脚本对用户实施攻击的一种手段。

最开始的时候,这种攻击是通过跨域来实现的,所以叫“跨域脚本”。但是发展到现在,往 HTML 文件中注入恶意代码的方式越来越多了,所以是否跨域注入脚本已经不是唯一的注入手段了,但是 XSS 这个名字却一直保留至今。

当页面被注入了恶意 JavaScript 脚本时,浏览器无法区分这些脚本是被恶意注入的还是正常的页面内容,所以恶意注入 JavaScript 脚本也拥有所有的脚本权限。

如果页面被注入了恶意 JavaScript 脚本,恶意脚本都能做哪些事情?

  • 可以窃取 Cookie 信息。恶意 JavaScript 通过“document.cookie”获取 Cookie 信息,然后通过 XMLHttpRequest 或者 Fetch 加上 CORS 功能将数据发送给恶意服务器;恶意服务器拿到用户的 Cookie 信息之后,就可以在其他电脑上模拟用户的登录,然后进行转账等操作。
  • 可以监听用户行为。恶意 JavaScript 使用“addEventListener”接口来监听键盘事件,比如可以获取用户输入的信用卡等信息,将其发送到恶意服务器。
  • 可以通过修改 DOM伪造假的登录窗口,用来欺骗用户输入用户名和密码等信息。
  • 可以在页面内生成浮窗广告,这些广告会严重地影响用户体验。

除了以上几种情况外,恶意脚本还能做很多其他的事情。总之,如果让页面插入了恶意脚本,那么就相当于把页面的隐私数据和行为完全暴露给黑客了。

恶意脚本的注入方式

网站开发者应该尽可能地避免页面中被注入恶意脚本。注入恶意脚本的方式:

  1. 存储型 XSS 攻击

image.png

存储型 XSS 攻击大致需要经过如下步骤:

  • 首先黑客利用站点漏洞将一段恶意 JavaScript 代码提交到网站的数据库中;
  • 然后用户向网站请求包含了恶意 JavaScript 脚本的页面;
  • 当用户浏览该页面的时候,恶意脚本就会将用户的 Cookie 信息等数据上传到黑客的服务器。

例子

2015 年喜马拉雅就被曝出了存储型 XSS 漏洞。起因是在用户设置专辑名称时,服务器对关键字过滤不严格,比如可以将专辑名称设置为一段 JavaScript,如下图所示:

image.png

当黑客将专辑名称设置为一段 JavaScript 代码并提交时,喜马拉雅的服务器会保存该段 JavaScript 代码到数据库中。然后当用户打开黑客设置的专辑时,这段代码就会在用户的页面里执行(如下图),这样就可以获取用户的 Cookie 等数据信息。

image.png

当用户打开黑客设置的专辑页面时,服务器也会将这段恶意 JavaScript 代码返回给用户,因此这段恶意脚本就在用户的页面中执行了。

恶意脚本可以通过 XMLHttpRequest 或者 Fetch 将用户的 Cookie 数据上传到黑客的服务器,如下图所示:

image.png

黑客拿到了用户 Cookie 信息之后,就可以利用 Cookie 信息在其他机器上登录该用户的账号(如下图),并利用用户账号进行一些恶意操作。

  1. 反射型 XSS 攻击

在反射型 XSS 攻击过程中,恶意 JavaScript 脚本属于用户发送给网站请求中的一部分,随后网站又把恶意 JavaScript 脚本返回给用户。当恶意 JavaScript 脚本在用户页面中被执行时,黑客就可以利用该脚本做一些恶意操作。

实例代码:

 var express = require('express');
 var router = express.Router();
 ​
 /* GET home page. */
 router.get('/', function (req, res, next) {
   res.render('index', { title: 'Express', xss: req.query.xss });
 });
 ​
 module.exports = router;

index.html

 <!DOCTYPE html>
 <html>
   <head>
     <title><%= title %></title>
     <link rel="stylesheet" href="/stylesheets/style.css" />
   </head>
   <body>
     <h1><%= title %></h1>
     <p>Welcome to <%= title %></p>
     <div><%- xss %></div>
   </body>
 </html>

服务端的作用是:将 URL 中 xss 参数的内容显示在页面。当打开http://localhost:3000/?xss=<script>alert('你被xss攻击了')</script>这段 URL 时,其结果如下图所示:

image.png 用户将一段含有恶意代码的请求提交给 Web 服务器,Web 服务器接收到请求时,又将恶意代码反射给了浏览器端,这就是反射型 XSS 攻击。在现实生活中,恶意链接的原理(所以可以查看连接的格式是否包含脚本)。

Web 服务器不会存储反射型 XSS 攻击的恶意脚本,这是和存储型 XSS 攻击不同的地方

反射型 XSS 攻击的原理

  1. 攻击者构造恶意 URL:攻击者将恶意 JavaScript 代码作为参数注入到 URL 中。例如:

     https://example.com/search?q=<script>alert('XSS');</script>
    

    在这个 URL 中,q 参数的值是恶意的 <script>alert('XSS');</script>

  2. 诱导用户访问恶意 URL:攻击者通过电子邮件、社交媒体、钓鱼网站等手段诱导用户点击该恶意 URL。

  3. 受害者访问恶意 URL:当受害者访问这个恶意 URL 时,服务器会将 URL 中的参数(包括恶意脚本)反射(返回)到响应页面中,并在浏览器中执行。

  4. 恶意脚本执行:由于恶意脚本被插入到了网页的响应中,浏览器会将其当成正常的 HTML 内容解析并执行。这样,攻击者的恶意代码就会在受害者的浏览器中运行,可能会窃取用户的 Cookie、敏感信息,或执行其他恶意操作。

反射型 XSS 的特点

  • 即时性:攻击发生在用户点击恶意链接并访问时。与存储型 XSS 不同,攻击代码不会长期存储在服务器上,而是立即反射并执行。
  • 需要诱导用户:反射型 XSS 依赖于攻击者诱导用户访问恶意链接,如果用户不访问或点击,该攻击就不会发生。

如何防御反射型 XSS 攻击

  1. 对用户输入进行严格的输入验证和输出编码

    • 输入验证:拒绝或过滤掉含有潜在危险的字符(如 <, >, ', " 等)的输入。
    • 输出编码:将所有用户输入在输出到 HTML 内容前进行适当的编码,例如使用 HTML 实体编码,将 < 编码为 &lt;> 编码为 &gt; 等。
  2. 使用 HTTP 安全头

    • 使用 Content-Security-Policy (CSP) HTTP 头,限制浏览器可以执行的脚本的来源,阻止不被信任的脚本执行。

       Content-Security-Policy: default-src 'self'; script-src 'self'
      
  3. 对 URL 参数进行编码和转义

    • 在处理 URL 参数时,确保对所有输出到页面的参数进行编码和转义。
  4. 避免在响应中直接输出用户输入的内容

    • 如果可能,避免在 HTML 响应中直接使用用户提供的输入。对需要展示的用户输入内容进行清理或替换。
  5. 使用现代的 Web 开发框架

    • 使用诸如 React、Vue.js 或 Angular 这类现代框架,它们默认会对用户输入进行转义和过滤,以防止 XSS 攻击。
  6. 利用内容安全策略 (CSP)

    • 设置合适的内容安全策略头部,指定可信任的脚本来源,从而防止执行未知来源的脚本。

反射型XSS生效的前提:服务器的对应接口能将用户提交的内容原样返回给客户端?

是的,反射型 XSS 攻击生效的前提之一是:服务器的某个接口或页面将用户提交的内容(例如通过 URL 参数或表单提交的数据)未经适当的过滤或转义,原样返回给客户端。这种情况下,恶意输入会直接反射(echo)到页面的响应中,导致恶意代码在用户浏览器中执行。

反射型 XSS 生效的条件

  1. 用户输入未经过滤或转义:服务器端代码未对用户输入进行适当的过滤或转义。例如,当用户提交数据时,服务器直接将数据插入到页面的响应中,而没有清理或编码潜在的危险字符。
  2. 服务器响应中包含用户提交的数据:用户的输入被直接插入到 HTML 内容中(例如作为文本节点、HTML 属性值、JavaScript 代码等),且浏览器会对这些数据进行解析和渲染。
  3. 恶意代码通过用户输入传递:攻击者可以构造包含恶意代码的请求(如 URL),诱导用户点击。当用户点击恶意链接时,服务器会返回一个包含恶意脚本的响应。

示例:反射型 XSS 的典型场景

假设一个网站 example.com 有一个搜索功能,用户可以在 URL 中输入搜索词,如:

 https://example.com/search?q=javascript

服务器将用户输入的 q 参数原样插入到 HTML 中返回:

 <!DOCTYPE html>
 <html>
 <head><title>Search Results</title></head>
 <body>
     <h1>Search Results for: javascript</h1>
     <!-- 其他内容 -->
 </body>
 </html>

如果攻击者构造了一个恶意的 URL,如:

 https://example.com/search?q=<script>alert('XSS');</script>

当用户点击这个链接时,服务器没有对 q 参数的内容进行任何过滤或转义,会直接将其插入响应中:

 <!DOCTYPE html>
 <html>
 <head><title>Search Results</title></head>
 <body>
     <h1>Search Results for: <script>alert('XSS');</script></h1>
     <!-- 其他内容 -->
 </body>
 </html>

此时,浏览器会解析并执行 <script>alert('XSS');</script>,导致弹出 XSS 警告框。这就是反射型 XSS 的攻击生效过程。

  1. 基于 DOM 的 XSS 攻击

基于 DOM 的 XSS 攻击(DOM-based XSS) 是一种在客户端(浏览器)上发生的跨站脚本攻击类型,它通过操纵 DOM(文档对象模型)来执行恶意代码。与反射型和存储型 XSS 不同,DOM 型 XSS 不涉及服务器端的参与,攻击者直接在客户端修改页面的内容或行为。

具体来讲,黑客通过各种手段将恶意脚本注入用户的页面中,比如通过网络劫持在页面传输过程中修改 HTML 页面的内容,这种劫持类型很多,有通过 WiFi 路由器劫持的,有通过本地恶意软件来劫持的,它们的共同点是在 Web 资源传输过程或者在用户使用页面的过程中修改 Web 页面的数据。

在 DOM 型 XSS 攻击中,恶意脚本不经过服务器返回,而是直接在客户端通过 JavaScript 操作 DOM 元素注入或执行。通常发生在以下情况下:

  • 页面从 URL、location.hashdocument.cookielocalStorage 等处读取数据并使用 JavaScript 将其插入到页面的 DOM 中,但未进行适当的安全处理。
  • JavaScript 动态修改页面内容(如 innerHTMLdocument.write 等)时,使用了用户可控的数据。

DOM 型 XSS 的典型例子

假设一个网站允许用户在 URL 中传递参数来控制显示的内容,并直接使用这些参数更新页面内容,而没有进行适当的转义和过滤。

示例 1:读取 URL 参数导致的 DOM 型 XSS

 <body>
   <h1>Welcome!</h1>
   <div id="message"></div>
   <script>
     // 从 URL 中获取 'msg' 参数的值
     const params = new URLSearchParams(window.location.search);
     const message = params.get('msg');
 ​
     // 直接将用户输入的参数插入到页面中,未经过任何过滤或转义
     document.getElementById('message').innerHTML = message;
   </script>
 </body>

攻击者可以构造一个恶意 URL,将恶意 JavaScript 代码注入 msg 参数中:

 http://example.com?msg=<script>alert('XSS');</script>

当用户访问这个 URL 时,浏览器将执行如下代码:

 <div id="message"><script>alert('XSS');</script></div>

此时,<script>alert('XSS');</script> 被插入到页面中并被执行,导致 XSS 攻击。

示例 2:读取 location.hash 导致的 DOM 型 XSS

 <body>
     <h1>Product Search</h1>
     <div id="search-results"></div>
     <script>
         // 获取 location.hash 中的内容
         const hash = window.location.hash.substring(1); // 去掉 '#' 符号
         // 将 hash 的内容插入到页面中
         document.getElementById('search-results').innerHTML = `Searching for: ${hash}`;
     </script>
 </body>

攻击者可以构造一个恶意 URL,将恶意 JavaScript 代码注入 location.hash 中:

 http://example.com/#<img src=x onerror=alert('XSS')>

当用户访问该 URL 时,window.location.hash 的值为 #<img src=x onerror=alert('XSS')>,然后浏览器将执行:

 <div id="search-results">Searching for: <img src=x onerror=alert('XSS')></div>

由于 <img> 标签中的 onerror 属性未经过安全处理,浏览器会执行 alert('XSS')

建议:

  1. 避免使用不安全的 DOM 操作:尽量避免使用 innerHTMLdocument.writeouterHTMLevalsetTimeout 等方法,这些方法容易引入恶意代码。使用更安全的替代方案,例如 textContentinnerText 来处理用户数据。

阻止XSS 攻击

储型 XSS 攻击和反射型 XSS 攻击都是需要经过 Web 服务器来处理,因此可以认为这两种类型的漏洞是服务端的安全漏洞。而基于 DOM 的 XSS 攻击全部都是在浏览器端完成的,因此基于 DOM 的 XSS 攻击是属于前端的安全漏洞。

但无论是何种类型的 XSS 攻击,它们都有一个共同点,那就是首先往浏览器中注入恶意脚本,然后再通过恶意脚本将用户信息发送至黑客部署的恶意服务器上。

所以要阻止 XSS 攻击,可以通过阻止恶意 JavaScript 脚本的注入和恶意消息的发送来实现。

常用的阻止 XSS 攻击的策略

  1. 服务器对输入脚本进行过滤或转码

不管是反射型还是存储型 XSS 攻击,都可以在服务器端将一些关键的字符进行转码,比如最典型的:

 code:<script>alert('你被 xss 攻击了')</script>

这段代码过滤后,只留下了:

 code:

这样,当用户再次请求该页面时,由于<script>标签的内容都被过滤了,所以这段脚本在客户端是不可能被执行的。

除了过滤之外,服务器还可以对这些内容进行转码,还是上面那段代码,经过转码之后,效果如下所示:

 code:&lt;script&gt;alert(&#39; 你被 xss 攻击了 &#39;)&lt;/script&gt;

经过转码之后的内容,如<script>标签被转换为&lt;script&gt;,因此即使这段脚本返回给页面,页面也不会执行这段脚本。

  1. 充分利用 CSP

实施严格的 CSP 可以有效地防范 XSS 攻击,具体来讲 CSP 有如下几个功能:

  • 限制加载其他域下的资源文件,这样即使黑客插入了一个 JavaScript 文件,这个 JavaScript 文件也是无法被加载的;
  • 禁止向第三方域提交数据,这样用户数据也不会外泄;
  • 禁止执行内联脚本和未授权的脚本;
  • 还提供了上报机制,这样可以帮助我们尽快发现有哪些 XSS 攻击,以便尽快修复问题。
  1. 使用 HttpOnly 属性

由于很多 XSS 攻击都是来盗用 Cookie 的,因此还可以通过使用 HttpOnly 属性来保护 Cookie 的安全。

通常服务器可以将某些 Cookie 设置为 HttpOnly 标志,HttpOnly 是服务器通过 HTTP 响应头来设置的,下面是打开 Google 时,HTTP 响应头中的一段:

 set-cookie: NID=189=M8q2FtWbsR8RlcldPVt7qkrqR38LmFY9jUxkKo3-4Bi6Qu_ocNOat7nkYZUTzolHjFnwBw0izgsATSI7TZyiiiaV94qGh-BzEYsNVa7TZmjAYTxYTOM9L_-0CN9ipL6cXi8l6-z41asXtm2uEwcOC5oh9djkffOMhWqQrlnCtOI; expires=Sat, 18-Apr-2020 06:52:22 GMT; path=/; domain=.google.com; HttpOnly

顾名思义,使用 HttpOnly 标记的 Cookie 只能使用在 HTTP 请求过程中,所以无法通过 JavaScript (无法通过 document.cookie 是来读取) 来读取这段 Cookie。我们还可以通过 Chrome 开发者工具来查看哪些 Cookie 被标记了 HttpOnly,如下图:

image-20220709164027335

  1. 通过添加验证码防止脚本冒充用户提交危险操作。
  2. 对于一些不受信任的输入,还可以限制其输入长度,可以增大 XSS 攻击的难度。

你认为前端开发者对 XSS 攻击应该负多大责任?

扩展:

CSP是可以通过meta标签设置的,如果恶意插入的是关于CSP的meta设置了?

如果攻击者能够在你的网页中插入恶意的 <meta> 标签来修改 CSP(内容安全策略),这表明网页已经遭受了 跨站脚本(XSS) 或类似的安全攻击。攻击者插入的恶意 <meta> 标签可以放宽或禁用原有的安全策略,从而进一步扩大攻击范围,例如加载恶意的脚本或窃取用户数据。

CSP 设置的方式及其风险

CSP 设置可以通过两种方式实现:

  1. HTTP 响应头

    • 服务器端通过设置 Content-Security-Policy HTTP 响应头来定义 CSP 策略。
    • 优点:无法被客户端篡改,因为 HTTP 头部由服务器发送,除非攻击者控制了服务器。
  2. HTML <meta> 标签

    • 可以在 HTML 文档的 <head> 部分使用 <meta http-equiv="Content-Security-Policy"> 来内嵌 CSP 策略。
    • 缺点:相较于 HTTP 响应头,使用 <meta> 标签设置 CSP 存在更高的被篡改风险,尤其是在页面内容可以被动态插入的情况下。

恶意插入 <meta> 标签的风险

  1. 放宽或禁用 CSP:如果攻击者能在你的 HTML 文档中插入一个 <meta> 标签来重新定义 CSP,他们可能会放宽 CSP 限制或完全禁用它,例如允许内联脚本('unsafe-inline')或者从不可信的来源加载资源。

     <meta http-equiv="Content-Security-Policy" content="default-src *; script-src 'unsafe-inline' *;">
    

    上述恶意 <meta> 标签的效果是允许从任何来源加载资源,并允许执行内联脚本。这使得 XSS 攻击或其他恶意行为更容易实现。

  2. 加载恶意资源:通过修改 CSP,攻击者可以指定从他们控制的服务器加载恶意脚本、样式表或其他资源。

  3. 逃避安全检测:如果 CSP 是为了满足某些安全合规性要求(如防止数据泄露),恶意修改 CSP 可以绕过这些安全措施,使得应用程序的数据和用户信息暴露给不可信的第三方。

如何防止恶意修改 CSP?

  1. 优先使用 HTTP 响应头设置 CSP

    • 设置 CSP 的最佳实践是通过服务器端发送的 HTTP 响应头,而不是通过 <meta> 标签。HTTP 响应头设置的 CSP 策略无法被客户端脚本篡改。
     Content-Security-Policy: default-src 'self'; script-src 'self'; img-src 'self'
    
  2. 防止 XSS 攻击

    • 如果攻击者可以插入或修改 <meta>标签,这通常意味着网站存在 XSS 漏洞。为了防止 XSS 攻击,建议采取以下措施:

      • 输入验证和输出编码:确保用户输入的内容在显示前进行适当的验证和编码,避免插入恶意脚本。
      • 使用安全的库和框架:利用现代的前端框架(如 React、Vue、Angular 等)默认的安全机制(如自动转义 HTML)来防止 XSS。
      • 启用 HTTP Only Cookie:通过设置 HttpOnly 标志,防止 JavaScript 访问敏感的 Cookie 信息。
  3. 启用子资源完整性 (Subresource Integrity, SRI)

    • SRI 是一种安全特性,用于确保外部资源(如脚本或样式表)未被篡改。使用 SRI,可以验证从可信来源加载的资源的完整性,即使 CSP 被恶意修改,攻击者的脚本也无法成功执行。
     <script src="https://example.com/script.js" integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxxNsqEC2I=" crossorigin="anonymous"></script>
    
  4. 利用浏览器扩展保护 CSP 设置

    • 某些浏览器扩展和安全工具可以保护 CSP 设置,防止网页被篡改。它们会检查 CSP 并在发现问题时提醒用户或阻止加载不安全的资源。

总结

  • CSP 的设置应通过服务器端的 HTTP 响应头来完成,而不是通过 <meta> 标签,这样可以防止客户端的篡改。
  • 如果恶意插入 <meta> 标签来修改 CSP 策略,这意味着网页存在 XSS 或其他严重的安全漏洞,应该优先修复这些安全问题。
  • 采取多层次的安全措施(如 XSS 防护、子资源完整性、内容安全策略等)来确保 Web 应用程序的安全性。