JavaScript安全:前端常见的XSS和CSRF攻击原理与防御措施。

JavaScript安全:前端常见的XSS和CSRF攻击原理与防御措施

大家好,今天我们来聊聊JavaScript前端安全中两个非常重要的威胁:跨站脚本攻击(XSS)和跨站请求伪造(CSRF)。我会深入讲解它们的原理,并提供实用的防御措施。

一、跨站脚本攻击 (XSS)

XSS 攻击允许攻击者将恶意 JavaScript 代码注入到其他用户的浏览器中。这些恶意代码可以窃取用户的 Cookie、会话信息,甚至可以模拟用户执行操作。

1.1 XSS 攻击的类型

主要有三种类型的 XSS 攻击:

  • 存储型 XSS (Stored XSS): 恶意脚本被永久存储在目标服务器上,例如数据库、留言板、博客评论等。当用户访问包含恶意脚本的页面时,脚本就会执行。

  • 反射型 XSS (Reflected XSS): 恶意脚本通过 URL 参数、POST 数据等方式传递给服务器,服务器未经处理直接返回给用户。当用户点击包含恶意脚本的链接或提交包含恶意脚本的表单时,脚本就会执行。

  • DOM 型 XSS (DOM-based XSS): 恶意脚本不经过服务器,完全在客户端执行。攻击者通过修改页面的 DOM 结构,使得恶意脚本被执行。

1.2 存储型 XSS 攻击示例

假设有一个简单的留言板应用,用户可以在上面发布留言。如果应用没有对用户输入进行合适的过滤,攻击者可以发布包含恶意 JavaScript 代码的留言:

<form id="messageForm">
  <textarea id="messageContent" name="message" placeholder="请输入留言"></textarea>
  <button type="submit">发布</button>
</form>

<div id="messageBoard">
  <!-- 留言列表 -->
</div>

<script>
  const messageForm = document.getElementById('messageForm');
  const messageBoard = document.getElementById('messageBoard');

  messageForm.addEventListener('submit', (event) => {
    event.preventDefault();
    const message = document.getElementById('messageContent').value;

    // 将留言添加到留言板
    const messageElement = document.createElement('div');
    messageElement.textContent = message;
    messageBoard.appendChild(messageElement);

    // 模拟后端存储 (实际场景中,留言会存储在数据库中)
    localStorage.setItem('messages', localStorage.getItem('messages') ? localStorage.getItem('messages') + message : message);

    // 清空输入框
    document.getElementById('messageContent').value = '';

    loadMessages();
  });

  // 加载留言
  function loadMessages() {
    messageBoard.innerHTML = '';
    const messages = localStorage.getItem('messages');
    if (messages) {
      const messageArray = messages.split(' '); // 简单分隔,用于模拟多个留言
      messageArray.forEach(message => {
        const messageElement = document.createElement('div');
        messageElement.innerHTML = message; // 注意:这里存在 XSS 漏洞!
        messageBoard.appendChild(messageElement);
      });
    }
  }

  loadMessages(); // 页面加载时加载留言
</script>

攻击者可以输入以下内容:

<script>alert('XSS Attack!')</script>

当其他用户访问这个留言板时,这段恶意脚本就会被执行,弹出一个警告框。更危险的是,攻击者可以使用这段脚本窃取用户的 Cookie 并发送到攻击者的服务器。

1.3 反射型 XSS 攻击示例

假设有一个搜索功能,用户可以通过 URL 参数传递搜索关键词。如果应用没有对 URL 参数进行合适的过滤,攻击者可以构造包含恶意 JavaScript 代码的 URL:

<form id="searchForm">
  <input type="text" id="searchInput" name="query" placeholder="搜索...">
  <button type="submit">搜索</button>
</form>

<div id="searchResults">
  <!-- 搜索结果 -->
</div>

<script>
  const searchForm = document.getElementById('searchForm');
  const searchResults = document.getElementById('searchResults');

  searchForm.addEventListener('submit', (event) => {
    event.preventDefault();
    const query = document.getElementById('searchInput').value;
    const url = `/search?query=${query}`; // 模拟 URL
    window.location.href = url;
  });

  // 从 URL 获取搜索关键词
  function getQueryParameter(name) {
    const url = window.location.href;
    name = name.replace(/[[]]/g, '\$&');
    const regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
      results = regex.exec(url);
    if (!results) return null;
    if (!results[2]) return '';
    return decodeURIComponent(results[2].replace(/+/g, ' '));
  }

  const searchQuery = getQueryParameter('query');

  if (searchQuery) {
    searchResults.innerHTML = `您搜索的是:${searchQuery}`; // 注意:这里存在 XSS 漏洞!
  }
</script>

攻击者可以构造以下 URL:

/search?query=<script>alert('XSS Attack!')</script>

当用户点击这个 URL 时,这段恶意脚本就会被执行,弹出一个警告框。

1.4 DOM 型 XSS 攻击示例

假设有一个页面,它使用 window.location.hash 来获取参数,并将其显示在页面上。

<div id="output"></div>

<script>
  const output = document.getElementById('output');
  output.innerHTML = decodeURIComponent(window.location.hash.substring(1)); // 注意:这里存在 XSS 漏洞!
</script>

攻击者可以构造以下 URL:

/page.html#<img src=x onerror=alert('XSS Attack!')>

当用户访问这个 URL 时,这段恶意脚本就会被执行,弹出一个警告框。因为 onerror 事件会在图片加载失败时触发。

1.5 XSS 防御措施

  • 输入验证和过滤 (Input Validation and Filtering): 对所有用户输入进行验证和过滤,包括来自表单、URL 参数、Cookie 等的数据。 验证数据类型、长度、格式等。 过滤掉或转义特殊字符,例如 <>"'& 等。

    • 白名单: 相比于黑名单,白名单更安全。只允许特定的字符或 HTML 标签。

    • 转义: 将特殊字符转换为 HTML 实体。例如,将 < 转换为 &lt;,将 > 转换为 &gt;

    function escapeHtml(string) {
      return string.replace(/[&<>"']/g, function(m) {
        switch (m) {
          case '&':
            return '&amp;';
          case '<':
            return '&lt;';
          case '>':
            return '&gt;';
          case '"':
            return '&quot;';
          case "'":
            return ''';
          default:
            return m;
        }
      });
    }
    
    // 示例
    const userInput = '<script>alert("XSS");</script>';
    const escapedInput = escapeHtml(userInput);
    console.log(escapedInput); // 输出:&lt;script&gt;alert(&quot;XSS&quot;);&lt;/script&gt;
  • 输出编码 (Output Encoding): 在将数据输出到 HTML 页面时,进行合适的编码。

    • HTML 编码: 用于将数据插入到 HTML 标签之间。使用 escapeHtml 函数进行编码。

    • URL 编码: 用于将数据插入到 URL 中。使用 encodeURIComponent 函数进行编码。

    • JavaScript 编码: 用于将数据插入到 JavaScript 代码中。需要更加小心,避免破坏 JavaScript 代码的语法。

  • 使用 Content Security Policy (CSP): CSP 是一种安全策略,可以限制浏览器可以加载的资源,从而减少 XSS 攻击的风险。

    • 可以通过 HTTP 响应头或 HTML <meta> 标签来设置 CSP。

    • 常用的 CSP 指令:

      • default-src: 设置所有资源的默认来源。

      • script-src: 设置 JavaScript 脚本的来源。

      • style-src: 设置 CSS 样式的来源。

      • img-src: 设置图片的来源。

      • connect-src: 设置 XMLHttpRequest、WebSocket 和 EventSource 连接的来源。

      • font-src: 设置字体的来源。

      • object-src: 设置 <object><embed><applet> 标签的来源。

      • media-src: 设置 <audio><video><track> 标签的来源。

      • frame-src: 设置 <iframe><frame> 标签的来源。

      • base-uri: 设置 <base> 标签的 URL。

      • form-action: 设置 <form> 标签的 action URL。

    <!-- 通过 meta 标签设置 CSP -->
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;">

    这个 CSP 策略表示:

    • default-src 'self': 默认情况下,只允许从同源加载资源。
    • script-src 'self' 'unsafe-inline': 允许从同源加载 JavaScript 脚本,并允许执行内联脚本。 ('unsafe-inline' 需要谨慎使用,因为它会降低 CSP 的安全性。)
    • style-src 'self' 'unsafe-inline': 允许从同源加载 CSS 样式,并允许执行内联样式。
    • img-src 'self' data:: 允许从同源加载图片,并允许使用 data URI。
  • 使用安全的 JavaScript 框架和库: 一些 JavaScript 框架和库,例如 React、Angular 和 Vue.js,提供了内置的 XSS 防御机制。它们会自动转义用户输入,并使用安全的 API 来操作 DOM。

  • 设置 HttpOnly Cookie: 通过设置 Cookie 的 HttpOnly 属性,可以防止 JavaScript 代码访问 Cookie。这可以防止攻击者通过 XSS 攻击窃取用户的 Cookie。

    // 在服务器端设置 HttpOnly Cookie
    document.cookie = "sessionId=123456789; HttpOnly";
  • 使用 Subresource Integrity (SRI): SRI 是一种安全机制,可以验证从 CDN 加载的资源的完整性。通过在 <script><link> 标签中添加 integrity 属性,可以确保浏览器加载的资源没有被篡改。

    <script src="https://example.com/script.js" integrity="sha384-oqVuAfW98GFTpiCZ6ipmnNWg3MGzcifm9IbX6Yqm6veY+nqUbfTIWuHqpcvvUvEA" crossorigin="anonymous"></script>

    integrity 属性的值是一个哈希值,用于验证资源的完整性。 crossorigin="anonymous" 属性用于允许跨域加载资源。

1.6 DOM 型 XSS 的特殊防御

DOM 型 XSS 由于完全在客户端发生,防御方式略有不同,更需要关注客户端代码的安全性。

  • 避免使用 eval() 函数: eval() 函数可以将字符串作为 JavaScript 代码执行,这很容易导致 XSS 攻击。

  • 避免直接操作 DOM: 尽量使用安全的 API 来操作 DOM,例如 textContent 代替 innerHTML

  • 小心处理 URL 参数: 对从 window.location.hashwindow.location.search 等获取的 URL 参数进行严格的验证和过滤。

二、跨站请求伪造 (CSRF)

CSRF 攻击允许攻击者冒充用户发起恶意请求。攻击者通过构造恶意链接或表单,诱导用户点击或提交,从而在用户不知情的情况下执行操作。

2.1 CSRF 攻击的原理

CSRF 攻击利用了浏览器会自动发送 Cookie 的特性。当用户登录到某个网站后,浏览器会保存该网站的 Cookie。当用户访问其他网站时,如果该网站构造了指向已登录网站的请求,浏览器会自动将 Cookie 发送给已登录网站。如果已登录网站没有对请求进行合适的验证,攻击者就可以冒充用户执行操作。

2.2 CSRF 攻击示例

假设有一个银行网站,用户可以通过以下 URL 转账:

/transfer?account=recipient&amount=100

攻击者可以构造一个包含以下代码的 HTML 页面:

<img src="/transfer?account=attacker&amount=1000000" width="0" height="0">

当用户访问这个页面时,浏览器会自动向银行网站发送转账请求,将 1000000 元转到攻击者的账户。如果银行网站没有对请求进行合适的验证,攻击者就可以成功地冒充用户转账。

2.3 CSRF 防御措施

  • 使用 CSRF Token: 在每个表单中添加一个随机生成的 CSRF Token。 服务器在处理请求时,验证请求中是否包含正确的 CSRF Token。

    • CSRF Token 应该足够随机,并且对每个用户和每个会话都是唯一的。

    • CSRF Token 可以存储在 Cookie 中,也可以存储在 Session 中。

    // 前端生成 CSRF Token
    function generateCSRFToken() {
      let token = '';
      const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
      for (let i = 0; i < 32; i++) {
        token += characters.charAt(Math.floor(Math.random() * characters.length));
      }
      return token;
    }
    
    const csrfToken = generateCSRFToken();
    localStorage.setItem('csrfToken', csrfToken);
    
    // 在表单中添加 CSRF Token
    const form = document.getElementById('myform');
    const hiddenField = document.createElement('input');
    hiddenField.type = 'hidden';
    hiddenField.name = 'csrfToken';
    hiddenField.value = csrfToken;
    form.appendChild(hiddenField);
    
    // 后端验证 CSRF Token (示例,具体实现取决于后端框架)
    function verifyCSRFToken(requestToken, storedToken) {
      return requestToken === storedToken;
    }
  • 使用 SameSite Cookie: SameSite Cookie 可以限制 Cookie 的跨域访问。

    • SameSite=Strict: Cookie 只能在同站点请求中使用。

    • SameSite=Lax: Cookie 可以在同站点请求中使用,也可以在 GET 请求中使用。

    • SameSite=None: Cookie 可以在跨站点请求中使用。 需要同时设置 Secure 属性,表示 Cookie 只能通过 HTTPS 连接发送。

    // 在服务器端设置 SameSite Cookie
    document.cookie = "sessionId=123456789; SameSite=Strict; Secure"; // 最安全的设置
    // 或者
     document.cookie = "sessionId=123456789; SameSite=Lax"; // 允许 GET 请求携带 Cookie
    //  需要HTTPS
    document.cookie = "sessionId=123456789; SameSite=None; Secure"; // 允许跨域,但必须使用 HTTPS
  • 验证 HTTP Referer 头部: HTTP Referer 头部包含了请求的来源 URL。 服务器可以验证 Referer 头部,确保请求来自可信的来源。 但是,Referer 头部可以被篡改,因此不能完全依赖它。

  • 双重 Cookie 验证 (Double Submit Cookie): 将 CSRF Token 同时存储在 Cookie 和表单中。 服务器在处理请求时,验证 Cookie 中的 CSRF Token 和表单中的 CSRF Token 是否一致。

    // 前端
    function setCookie(name, value, days) {
      let expires = "";
      if (days) {
        let date = new Date();
        date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
        expires = "; expires=" + date.toUTCString();
      }
      document.cookie = name + "=" + (value || "") + expires + "; path=/";
    }
    
    function getCookie(name) {
      let nameEQ = name + "=";
      let ca = document.cookie.split(';');
      for (let i = 0; i < ca.length; i++) {
        let c = ca[i];
        while (c.charAt(0) == ' ') c = c.substring(1, c.length);
        if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length);
      }
      return null;
    }
    
    const csrfToken = generateCSRFToken();
    setCookie('csrfToken', csrfToken, 7); // 设置 Cookie
    localStorage.setItem('csrfToken', csrfToken);
    
    // 在表单中添加 CSRF Token
    const form = document.getElementById('myform');
    const hiddenField = document.createElement('input');
    hiddenField.type = 'hidden';
    hiddenField.name = 'csrfToken';
    hiddenField.value = csrfToken;
    form.appendChild(hiddenField);
    
    // 后端
    function verifyDoubleSubmitCookie(cookieToken, formToken) {
      return cookieToken === formToken && cookieToken !== null && formToken !== null;
    }
    
    // 示例
    const cookieToken = getCookie('csrfToken');
    const formToken = request.body.csrfToken; // 假设从请求体中获取
    
    if (verifyDoubleSubmitCookie(cookieToken, formToken)) {
      // 处理请求
      console.log('CSRF 验证通过');
    } else {
      // 拒绝请求
      console.log('CSRF 验证失败');
    }
  • 避免使用 GET 请求执行敏感操作: GET 请求容易被 CSRF 攻击,因为攻击者可以通过构造 <img> 标签或 <a> 标签来发起 GET 请求。 应该使用 POST 请求来执行敏感操作。

  • 用户教育: 教育用户不要轻易点击不明链接,不要访问不信任的网站。

三、总结

攻击类型 原理 防御措施
XSS 将恶意 JavaScript 代码注入到其他用户的浏览器中。 输入验证和过滤,输出编码,使用 CSP,使用安全的 JavaScript 框架和库,设置 HttpOnly Cookie,使用 SRI,避免使用 eval(),避免直接操作 DOM,小心处理 URL 参数。
CSRF 冒充用户发起恶意请求。 使用 CSRF Token,使用 SameSite Cookie,验证 HTTP Referer 头部,双重 Cookie 验证,避免使用 GET 请求执行敏感操作,用户教育。

四、提升安全意识,构建更安全的应用

XSS 和 CSRF 都是非常常见的 Web 安全威胁。 开发者需要充分了解它们的原理,并采取合适的防御措施,才能保护用户的数据安全。 加强安全意识,定期进行安全测试,及时修复漏洞,是构建更安全 Web 应用的关键。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注