深入解析XSS攻击:从原理到防御的全方位指南

234 阅读5分钟

XSS

一、什么是XSS?

  1. Cross-Site Scripting(跨站脚本攻击)为了与CSS作区分故简称 XSS,是一种代码注入攻击。攻击者通过在目标网站上注入恶意脚本,使之在用户的浏览器上运行。利用这些恶意脚本,攻击者可获取用户的敏感信息如 Cookie、SessionID 等,进而危害数据安全。
  2. 本质
    • 恶意代码未经过滤,与网站正常的代码混在一起;浏览器无法分辨哪些脚本是可信的,导致恶意脚本被执行。
  3. 可能被注入恶意脚本的内容
    • 来自用户的 UGC 信息
    • 来自第三方的链接
    • URL 参数
    • POST 参数
    • Referer (可能来自不可信的来源)
    • Cookie (可能来自其他子域注入)

二、XSS攻击方式

  1. 分为三种:存储型 、反射型 、DOM 型

  2. 存储型XSS
    • 不需要用户手动触发,所有浏览者访问页面时都会被XSS
    • 常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等 在这里插入图片描述
  3. 反射型XSS
    • 需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击
    • 常见于通过 URL 传递参数的功能,如网站搜索、跳转等 在这里插入图片描述
  4. DOM型XSS
    • 属于前端 JavaScript 自身的安全漏洞 在这里插入图片描述
  5. 区别
    特征存储型 XSS反射型 XSSDOM 型 XSS
    存储位置恶意代码存储在服务器数据库恶意代码不存储,仅存在于URL参数恶意代码不存储,存在于URL片段
    触发条件用户访问被感染的页面用户点击包含恶意参数的链接用户点击包含恶意片段(#)的链接
    数据流服务器 → 用户浏览器服务器反射参数 → 用户浏览器完全在客户端处理(不经过服务器)
    持久性长期存在(直到数据被删除)一次性(仅当用户点击链接时)一次性(仅当用户点击链接时)
    攻击范围所有访问该页面的用户仅点击特定链接的用户仅点击特定链接的用户

三、常见的XSS攻击场景

  1. 社交媒体评论——存储型XSS
    • 攻击者在评论区提交恶意脚本(如 <script>sendCookiesToAttacker()</script>),该评论被存储到服务器。其他用户访问该页面时,恶意脚本自动加载并窃取其会话Cookie。
    <!-- 用户评论内容 -->
    <div class="comment">
      这篇文章写得真好!<script>sendCookiesToAttacker()</script>
    </div>
    
  2. 搜索功能——反射型XSS
    • 网站搜索功能未过滤输入参数,攻击者构造恶意链接:https://example.com/search?q=<script>alert('XSS攻击')</script>,用户点击链接后,服务器返回的页面包含未过滤的恶意脚本。
  3. 单页面应用路由——DOM型XSS
    • SPA前端路由根据URL参数动态加载内容,但未对参数过滤。攻击者构造链接:https://example.com/#/profile?username=<script>alert('XSS攻击')</script>,用户点击链接前端脚本username 参数直接渲染到页面,触发XSS。

四、防御策略

  1. 预防存储型XSS

    • 输入过滤
      • 对于明确的输入类型

        • 如数字、URL、电话号码、邮件,使用escapeHTML()可以把用户的输入内容进行转义
        原始字符转义后实体编码
        <&lt
        >&gt
        &&amp
        "&quot
        '&#x27
      // 用户输入:<script>alert('XSS攻击')</script>
      const escaped = escapeHTML(userInput);
      document.getElementById('content').innerHTML = escaped;
      // 输出:&lt;script&gt;alert('XSS攻击')&lt;/script&gt;
      
      • 更推荐使用成熟且完善的现有库
      // 使用 DOMPurify(支持更复杂的净化)
      import DOMPurify from 'dompurify';
      DOMPurify.sanitize(userInput, { ALLOWED_TAGS: [] }); 
      
      • 特殊场景补充
      场景额外需要转义的字符方法
      URL参数% #配合使用 encodeURIComponent
      CSS属性\ ()使用 CSS.escape()
      JSON数据\u2028 \u2029JSON.stringify
    • 对于不明确的类型

      • 最好不要使用输入过滤,因为当把转义后的数据发送到后端再回到前端,赋值给一个变量之后,展示出来的效果会变成乱码(如5<7会变成5&lt7
  2. 预防存储型XSS和反射型XSS

    • 纯前端渲染
      • 现代框架的自动防护

      • React:默认转义所有插值内容

        function UserContent({ text }) {
          return <div>{text}</div>;  // 自动转义 `<` `>` 等字符
        }
        // 用户输入 `<script>alert('XSS攻击')</script>` 会显示为文本,不会执行
        
      • Vue:模板插值自动编码

        <template>
          <div>{{ userInput }}</div>  <!-- 自动转换为文本 -->
        </template>
        
      • Angular:插值绑定安全处理

        <div>{{ userInput }}</div>  <!-- 输出内容自动转义 -->
        
      安全API危险替代品场景
      textContentinnerHTML插入纯文本内容
      setAttributeinnerHTML设置元素属性
      document.createElement + appendChildinsertAdjacentHTML动态创建节点
    • 转义HTML
      • 使用更完善更细致的转义库org.owasp.encoder,以下代码引用自 org.owasp.encoder 的官方说明

        <!-- HTML 标签内文字内容 -->
        <div><%= Encode.forHtml(UNTRUSTED) %></div>
        
        <!-- HTML 标签属性值 -->
        <input value="<%= Encode.forHtml(UNTRUSTED) %>" />
        
        <!-- CSS 属性值 -->
        <div style="width:<= Encode.forCssString(UNTRUSTED) %>">
        
        <!-- CSS URL -->
        <div style="background:<= Encode.forCssUrl(UNTRUSTED) %>">
        
        <!-- JavaScript 内联代码块 -->
        <script>
          var msg = "<%= Encode.forJavaScript(UNTRUSTED) %>";
          alert(msg);
        </script>
        
        <!-- JavaScript 内联代码块内嵌 JSON -->
        <script>
        var __INITIAL_STATE__ = JSON.parse('<%= Encoder.forJavaScript(data.to_json) %>');
        </script>
        
        <!-- HTML 标签内联监听器 -->
        <button
          onclick="alert('<%= Encode.forJavaScript(UNTRUSTED) %>');">
          click me
        </button>
        
        <!-- URL 参数 -->
        <a href="/search?value=<%= Encode.forUriComponent(UNTRUSTED) %>&order=1#top">
        
        <!-- URL 路径 -->
        <a href="/page/<%= Encode.forUriComponent(UNTRUSTED) %>">
        
        <!--
          URL.
          注意:要根据项目情况进行过滤,禁止掉 "javascript:" 链接、非法 scheme 等
        -->
        <a href='<%=
          urlValidator.isValid(UNTRUSTED) ?
            Encode.forHtml(UNTRUSTED) :
            "/404"
        %>'>
          link
        </a>
        
        
  3. 预防DOM型XSS

    • 使用.innerHTML.outerHTMLdocument.write() 时要特别小心,不要把不可信的数据作为 HTML 插到页面上,而应尽量使用 .textContent.setAttribute()
    • DOM 中的内联事件监听器,如 locationonclickonerroronloadonmouseover 等,<a> 标签的 href 属性,JavaScript 的 eval()setTimeout()setInterval() 等,都能把字符串作为代码运行
  4. 其他预防方法

    • httpOnly: 在 cookie 中设置 HttpOnly 属性后,js脚本将无法读取到 cookie 信息。

      // 后端实现
      app.post('/login', (req, res) => {
        res.cookie('sessionID', 'xxxx', {
          httpOnly: true,  // 关键:禁止JS访问
          secure: true,    // 仅通过HTTPS传输
          sameSite: 'Lax'  // 防御CSRF
        });
        res.send('登录成功');
      });
      
    • 白名单:

      • 输入白名单验证

        function validateInput(input) {
            // 只允许字母、数字、空格以及特定的标签
            const whiteListPattern = /^[a-zA-Z0-9\s<>/bB/iI]*$/;
            return whiteListPattern.test(input);
        }
        const userInput = "<script>alert('xss')</script>";
        if (!validateInput(userInput)) {
            // 处理非法输入
            console.log("Invalid input!");
        }
        
      • HTML标签/属性白名单

        // 使用DOMPurify库配置白名单
        import DOMPurify from 'dompurify';
        
        const dirtyHTML = '<b class="safe">合法内容</b><script>恶意代码</script>';
        const cleanHTML = DOMPurify.sanitize(dirtyHTML, {
          ALLOWED_TAGS: ['b', 'i', 'em'],     // 允许的标签
          ALLOWED_ATTR: ['class', 'style'],   // 允许的属性
          FORBID_TAGS: ['style', 'script']    // 强制禁止的标签
        });
        
        console.log(cleanHTML); 
        // 输出:<b class="safe">合法内容</b>
        
    • CSP:

      • 基础配置

        Content-Security-Policy:
          default-src 'self';     # 默认只允许同源资源
          script-src 'self'       # 脚本仅限同源
            https://trusted.cdn.com 
            'nonce-abc123';       # 允许带特定nonce的内联脚本
          style-src 'self' 'unsafe-inline'; # 允许内联样式(慎用)
          img-src * data:;        # 允许所有图片源(根据需求收紧)
          font-src 'self'; 
          frame-src 'none';       # 禁止嵌入iframe
          report-uri /csp-report; # 违规报告地址
        

参考资料 juejin.cn/post/684490… juejin.cn/post/684490…