探讨前端安全性中的 CSRF, XSS, 点击劫持等攻击原理和防御措施,以及如何在 JavaScript 代码层面进行防范。

各位听众,大家好!我是今天的主讲人。今天咱们来聊聊前端安全那些事儿,保证让大家听得懂、学得会、用得上,以后写代码也能更安心!

前端安全攻防战:CSRF、XSS、点击劫持,一个都别想跑!

前端安全,听起来高大上,实际上就是保护咱们用户的隐私和数据不被坏人偷走或者篡改。这年头,网络安全威胁可不小,咱们前端工程师得像个战士一样,守好这道防线。今天,我们就重点聊聊三个常见的攻击:CSRF、XSS 和点击劫持,以及如何在 JavaScript 代码层面进行防范。

第一战:CSRF(跨站请求伪造)——“李鬼”冒充“李逵”

啥是 CSRF?

CSRF,英文全称 Cross-Site Request Forgery,翻译过来就是“跨站请求伪造”。简单来说,就是攻击者伪装成你的用户,偷偷地向你的服务器发送请求,执行一些你用户并不想执行的操作。

想象一下:你登录了某个银行网站,正在浏览账户余额。这时,你点开了一个恶意链接,这个链接指向银行网站的转账接口,并带上了你的 Cookie 信息。你的浏览器一看,这个请求是发给银行网站的,而且带着你的 Cookie,就屁颠屁颠地发送了过去。银行服务器一看,请求来自你的浏览器,Cookie 也匹配,就以为是你本人发起的请求,于是就按照链接里的指令,把你的钱转到了攻击者的账户里。

是不是想想都后怕?

CSRF 攻击原理

CSRF 攻击之所以能成功,是因为它利用了浏览器的“同源策略”的漏洞。浏览器在发送请求时,会自动带上与目标域名相关的 Cookie 信息。攻击者只要诱骗用户访问包含恶意请求的页面,就能利用用户的 Cookie,冒充用户向服务器发送请求。

如何防御 CSRF?

防御 CSRF 的关键在于,让服务器能够区分请求是否来自真正的用户操作。以下是一些常见的防御方法:

  1. 使用 CSRF Token

    CSRF Token 是一种常见的防御方法。它的原理是,在用户访问页面时,服务器生成一个随机的 Token,并将它放在 Session 或者 Cookie 中。同时,将这个 Token 嵌入到表单或者链接中。当用户提交表单或者点击链接时,浏览器会将 Token 一起发送给服务器。服务器验证 Token 的有效性,如果 Token 不正确,就拒绝请求。

    代码示例(前端):

    // 获取 CSRF Token (假设 Token 已经通过某种方式传递到前端,比如隐藏的 input 字段)
    const csrfToken = document.querySelector('input[name="_csrf"]').value;
    
    // 创建一个 XMLHttpRequest 对象
    const xhr = new XMLHttpRequest();
    
    // 设置请求头,将 CSRF Token 包含在请求头中
    xhr.setRequestHeader('X-CSRF-Token', csrfToken);
    
    // 发送请求
    xhr.open('POST', '/api/transfer', true);
    xhr.setRequestHeader('Content-Type', 'application/json');
    xhr.onload = function() {
      // 处理响应
    };
    xhr.send(JSON.stringify({ amount: 100, toAccount: 'attacker' }));

    代码示例(后端,Node.js + Express):

    const express = require('express');
    const csrf = require('csurf');
    const cookieParser = require('cookie-parser');
    
    const app = express();
    
    // 使用 cookie-parser 中间件
    app.use(cookieParser());
    
    // 使用 csrf 中间件
    const csrfProtection = csrf({ cookie: true });
    app.use(csrfProtection);
    
    // 渲染包含 CSRF Token 的页面
    app.get('/transfer', (req, res) => {
      res.send(`
        <form action="/transfer" method="POST">
          <input type="hidden" name="_csrf" value="${req.csrfToken()}">
          <input type="number" name="amount" placeholder="Amount">
          <input type="text" name="toAccount" placeholder="To Account">
          <button type="submit">Transfer</button>
        </form>
      `);
    });
    
    // 处理转账请求
    app.post('/transfer', (req, res) => {
      if (req.body._csrf !== req.csrfToken()) {
        return res.status(403).send('CSRF validation failed');
      }
      // 处理转账逻辑
      res.send('Transfer successful!');
    });
    
    app.listen(3000, () => {
      console.log('Server listening on port 3000');
    });

    注意事项:

    • Token 的生成必须是随机的、不可预测的。
    • Token 必须保存在服务器端,防止泄露。
    • Token 必须在使用后失效,防止重放攻击。
    • 对于 AJAX 请求,需要将 Token 放在请求头中,而不是放在请求体中,以防止被浏览器缓存。
  2. 验证 HTTP Referer 字段

    HTTP Referer 字段记录了请求的来源地址。服务器可以验证 Referer 字段,判断请求是否来自合法的域名。如果 Referer 字段为空或者不是合法的域名,就拒绝请求。

    代码示例(后端,Node.js + Express):

    const express = require('express');
    const app = express();
    
    app.post('/transfer', (req, res) => {
      const referer = req.headers.referer;
      if (!referer || !referer.startsWith('http://your-domain.com')) {
        return res.status(403).send('Invalid Referer');
      }
      // 处理转账逻辑
      res.send('Transfer successful!');
    });
    
    app.listen(3000, () => {
      console.log('Server listening on port 3000');
    });

    注意事项:

    • Referer 字段可以被伪造,因此这种方法并不是完全可靠。
    • 某些浏览器可能会禁用 Referer 字段,导致验证失败。
  3. 使用双重 Cookie 验证

    双重 Cookie 验证的原理是,在用户访问页面时,服务器生成一个随机的 Token,并将它放在 Cookie 中。同时,将这个 Token 嵌入到表单或者链接中。当用户提交表单或者点击链接时,浏览器会将 Cookie 中的 Token 和表单中的 Token 一起发送给服务器。服务器验证两个 Token 是否一致,如果一致,就认为请求是合法的。

    代码示例(前端):

    // 获取 Cookie 中的 CSRF Token
    function getCookie(name) {
      const value = `; ${document.cookie}`;
      const parts = value.split(`; ${name}=`);
      if (parts.length === 2) return parts.pop().split(';').shift();
    }
    
    const csrfToken = getCookie('csrfToken');
    
    // 创建一个 XMLHttpRequest 对象
    const xhr = new XMLHttpRequest();
    
    // 设置请求头,将 CSRF Token 包含在请求头中
    xhr.setRequestHeader('X-CSRF-Token', csrfToken);
    
    // 发送请求
    xhr.open('POST', '/api/transfer', true);
    xhr.setRequestHeader('Content-Type', 'application/json');
    xhr.onload = function() {
      // 处理响应
    };
    xhr.send(JSON.stringify({ amount: 100, toAccount: 'attacker' }));

    代码示例(后端,Node.js + Express):

    const express = require('express');
    const cookieParser = require('cookie-parser');
    const crypto = require('crypto');
    
    const app = express();
    
    // 使用 cookie-parser 中间件
    app.use(cookieParser());
    
    // 设置 CSRF Token
    app.use((req, res, next) => {
      if (!req.cookies.csrfToken) {
        const csrfToken = crypto.randomBytes(64).toString('hex');
        res.cookie('csrfToken', csrfToken, { httpOnly: true, secure: true });
      }
      next();
    });
    
    // 处理转账请求
    app.post('/transfer', (req, res) => {
      const csrfTokenFromCookie = req.cookies.csrfToken;
      const csrfTokenFromHeader = req.headers['x-csrf-token'];
    
      if (!csrfTokenFromCookie || !csrfTokenFromHeader || csrfTokenFromCookie !== csrfTokenFromHeader) {
        return res.status(403).send('CSRF validation failed');
      }
    
      // 处理转账逻辑
      res.send('Transfer successful!');
    });
    
    app.listen(3000, () => {
      console.log('Server listening on port 3000');
    });

    注意事项:

    • Cookie 必须设置 httpOnly 属性,防止被 JavaScript 读取。
    • Cookie 必须设置 secure 属性,只允许在 HTTPS 连接中传输。
  4. 使用 SameSite Cookie 属性

    SameSite Cookie 属性可以限制 Cookie 的作用域,防止跨站请求携带 Cookie。它可以设置为以下三个值:

    • Strict: 只有在同一站点下的请求才会携带 Cookie。
    • Lax: 在同一站点下的请求以及部分跨站请求(例如 <a> 标签发起的 GET 请求)会携带 Cookie。
    • None: 允许所有请求携带 Cookie。但是,如果设置为 None,必须同时设置 secure 属性。

    代码示例(后端,Node.js + Express):

    const express = require('express');
    const cookieParser = require('cookie-parser');
    
    const app = express();
    
    // 使用 cookie-parser 中间件
    app.use(cookieParser());
    
    // 设置 Cookie
    app.get('/set-cookie', (req, res) => {
      res.cookie('myCookie', 'myValue', { sameSite: 'Strict', secure: true });
      res.send('Cookie set');
    });
    
    app.listen(3000, () => {
      console.log('Server listening on port 3000');
    });

    注意事项:

    • SameSite Cookie 属性的兼容性不是很好,需要考虑浏览器的兼容性。

CSRF 防御总结

防御方法 优点 缺点
CSRF Token 安全性高,能够有效防止 CSRF 攻击。 需要在每个表单和链接中添加 Token,增加了开发成本。需要保证 Token 的安全性,防止泄露。
Referer 验证 实现简单,成本低。 Referer 字段可以被伪造,安全性较低。某些浏览器可能会禁用 Referer 字段,导致验证失败。
双重 Cookie 验证 安全性较高,能够有效防止 CSRF 攻击。 需要维护两个 Cookie,增加了复杂性。Cookie 必须设置 httpOnly 属性,防止被 JavaScript 读取。Cookie 必须设置 secure 属性,只允许在 HTTPS 连接中传输。
SameSite Cookie 属性 实现简单,能够有效防止跨站请求携带 Cookie。 兼容性不是很好,需要考虑浏览器的兼容性。如果设置为 None,必须同时设置 secure 属性。

第二战:XSS(跨站脚本攻击)——“木马”潜入“卧室”

啥是 XSS?

XSS,英文全称 Cross-Site Scripting,翻译过来就是“跨站脚本攻击”。它是一种代码注入攻击,攻击者通过在网页中注入恶意的脚本代码,当用户浏览网页时,这些脚本代码会被执行,从而窃取用户的 Cookie、session 等敏感信息,或者篡改网页内容,甚至控制用户的浏览器。

想象一下:你正在浏览一个论坛,看到一个帖子,里面包含了一段看似无害的代码。当你浏览这个帖子时,这段代码被执行,偷偷地把你的 Cookie 发送到了攻击者的服务器上。攻击者利用你的 Cookie,就可以冒充你登录论坛,发布恶意信息,甚至修改你的个人资料。

XSS 攻击原理

XSS 攻击之所以能成功,是因为网站没有对用户输入的数据进行充分的验证和过滤,导致攻击者注入的脚本代码被当做正常的 HTML 代码执行。

XSS 分为三种类型:

  1. 存储型 XSS(Persistent XSS)

    存储型 XSS 是最危险的一种 XSS 攻击。攻击者将恶意脚本存储在服务器的数据库中,当用户访问包含恶意脚本的页面时,恶意脚本会被执行。

    攻击流程:

    1. 攻击者在网站的留言板、评论区等地方发布包含恶意脚本的内容。
    2. 恶意脚本被存储在服务器的数据库中。
    3. 其他用户访问包含恶意脚本的页面时,恶意脚本被执行。
  2. 反射型 XSS(Reflected XSS)

    反射型 XSS 是一种非持久性的 XSS 攻击。攻击者将恶意脚本作为请求参数发送给服务器,服务器将恶意脚本作为响应内容返回给浏览器,浏览器执行恶意脚本。

    攻击流程:

    1. 攻击者构造包含恶意脚本的 URL。
    2. 攻击者诱骗用户点击该 URL。
    3. 浏览器向服务器发送包含恶意脚本的请求。
    4. 服务器将恶意脚本作为响应内容返回给浏览器。
    5. 浏览器执行恶意脚本。
  3. DOM 型 XSS(DOM-based XSS)

    DOM 型 XSS 是一种基于 DOM 的 XSS 攻击。攻击者通过修改 DOM 树,将恶意脚本注入到网页中。

    攻击流程:

    1. 攻击者构造包含恶意脚本的 URL。
    2. 攻击者诱骗用户点击该 URL。
    3. 浏览器执行 JavaScript 代码,修改 DOM 树。
    4. 恶意脚本被注入到网页中。

如何防御 XSS?

防御 XSS 的关键在于,对用户输入的数据进行充分的验证和过滤,防止攻击者注入恶意脚本。以下是一些常见的防御方法:

  1. 输入验证

    对用户输入的数据进行验证,只允许输入符合预期格式的数据。例如,如果用户输入的是邮箱地址,就验证是否符合邮箱地址的格式。如果用户输入的是数字,就验证是否为数字。

    代码示例(前端):

    const emailInput = document.getElementById('email');
    const email = emailInput.value;
    
    // 验证邮箱地址的格式
    const emailRegex = /^[^s@]+@[^s@]+.[^s@]+$/;
    if (!emailRegex.test(email)) {
      alert('Invalid email address');
      return;
    }

    代码示例(后端,Node.js + Express):

    const express = require('express');
    const validator = require('validator');
    
    const app = express();
    
    app.post('/register', (req, res) => {
      const email = req.body.email;
    
      // 验证邮箱地址的格式
      if (!validator.isEmail(email)) {
        return res.status(400).send('Invalid email address');
      }
    
      // 处理注册逻辑
      res.send('Registration successful!');
    });
    
    app.listen(3000, () => {
      console.log('Server listening on port 3000');
    });
  2. 输出编码

    对用户输入的数据进行编码,将特殊字符转换为 HTML 实体。例如,将 < 转换为 &lt;,将 > 转换为 &gt;,将 " 转换为 &quot;,将 ' 转换为 ',将 & 转换为 &amp;

    代码示例(前端):

    function escapeHtml(str) {
      const entityMap = {
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": ''',
        '/': '/'
      };
    
      return String(str).replace(/[&<>"'/]/g, function (s) {
        return entityMap[s];
      });
    }
    
    const userInput = '<script>alert("XSS");</script>';
    const escapedInput = escapeHtml(userInput);
    document.getElementById('output').innerHTML = escapedInput; // 显示 "<script>alert("XSS");</script>"

    代码示例(后端,Node.js + Express):

    
    const express = require('express');
    const h

发表回复

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