首页/文章/前端

如何防止 XSS、CSRF 等常见的安全漏洞

2025-03-13
25829 分钟
...

前言

随着互联网技术的飞速发展,前端应用的功能越来越强大,用户体验也越来越好。然而,这也使得前端应用面临更多的安全威胁。其中,跨站脚本攻击(XSS)和跨站请求伪造(CSRF)是最常见且危害较大的两种攻击方式。本文将详细介绍这两种攻击的原理,并提供详细的防范措施,帮助开发者构建更加安全的前端应用。


一、跨站脚本攻击(XSS)

1.1 原理

跨站脚本攻击(Cross-Site Scripting,简称 XSS)是一种攻击手段,攻击者通过在网页中插入恶意脚本,当其他用户浏览该网页时,这些恶意脚本会在用户的浏览器中执行,从而盗取用户信息、篡改网页内容或进行其他恶意操作。XSS 攻击主要分为三种类型:

  1. 反射型 XSS:恶意脚本通过 URL 参数或其他输入点传递到服务器,然后直接返回给用户。这种类型的 XSS 攻击通常发生在搜索结果页、错误消息页等动态生成的页面上。
  2. 存储型 XSS:恶意脚本被存储在服务器上,当其他用户访问相关页面时,脚本会被执行。这种类型的 XSS 攻击常见于评论系统、论坛、博客等允许用户提交内容的场景。
  3. DOM 型 XSS:恶意脚本通过修改页面的 DOM 结构来执行,通常发生在客户端 JavaScript 代码中。这种类型的 XSS 攻击不涉及服务器端的处理,完全在客户端发生。
1.2 防范措施
  1. 输入验证和过滤
    输入验证和过滤是防止 XSS 攻击的第一道防线。开发者需要对用户提交的所有数据进行严格的验证和过滤,确保数据中不包含恶意脚本。常用的验证方法包括正则表达式匹配、黑名单过滤等。

    function sanitizeInput(input) {
      // 使用正则表达式过滤掉可能的恶意脚本
      return input.replace(/[<>&"']/g, function(char) {
        return '&#' + char.charCodeAt(0) + ';'
      })
    }
  2. 输出编码
    在将数据输出到 HTML 页面时,对数据进行适当的编码,可以有效防止恶意脚本的执行。常见的编码方法包括 HTML 实体编码、JavaScript 编码等。

    function escapeHtml(unsafe) {
      return unsafe
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#039;")
    }
    
    const userComment = "<script>alert('XSS');</script>"
    const safeComment = escapeHtml(userComment)
    document.getElementById('comment').innerHTML = safeComment
  3. Content Security Policy (CSP)
    Content Security Policy (CSP) 是一种安全机制,用于限制网页可以加载的资源。通过设置 CSP 头,可以有效地防止恶意脚本的执行。CSP 头可以通过 HTTP 响应头或 <meta> 标签设置。

    Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://trusted.cdn.com; style-src 'self' 'unsafe-inline' https://trusted.cdn.com
    
    
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://trusted.cdn.com; style-src 'self' 'unsafe-inline' https://trusted.cdn.com">
  4. HTTPOnly Cookie
    设置 Cookie 的 HttpOnly 标志,可以防止 JavaScript 访问 Cookie,从而减少 XSS 攻击的风险。HttpOnly 标志可以在设置 Cookie 时添加。

    Set-Cookie: session=abc123; HttpOnly
  5. X-XSS-Protection
    X-XSS-Protection 是一种浏览器内置的防护机制,可以自动检测并阻止某些类型的 XSS 攻击。虽然现代浏览器已经逐步淘汰了这个头,但在一些旧版浏览器中仍然有效。

    X-XSS-Protection: 1; mode=block
  6. X-Content-Type-Options
    设置 X-Content-Type-Options 头为 nosniff,可以防止浏览器猜测 MIME 类型,从而减少因 MIME 类型误判导致的 XSS 攻击。

    X-Content-Type-Options: nosniff

二、跨站请求伪造(CSRF)

2.1 原理

跨站请求伪造(Cross-Site Request Forgery,简称 CSRF)是一种攻击手段,攻击者诱导用户在已登录的网站上执行非预期的操作。攻击者通过构造一个恶意请求,利用用户已有的认证信息(如 Cookie),在用户不知情的情况下提交表单或执行其他操作。CSRF 攻击的关键在于攻击者能够预测用户的认证信息,并构造出有效的请求。

2.2 防范措施
  1. CSRF Token
    在表单中添加一个随机生成的 CSRF Token,并在服务器端进行验证。每次请求时,服务器会检查 CSRF Token 是否有效,从而防止非法请求。CSRF Token 可以通过隐藏字段、HTTP 头等方式传递。

    <form action="/submit" method="POST">
      <input type="hidden" name="csrfToken" value="{{ csrfToken }}">
      <input type="text" name="username" placeholder="Username">
      <button type="submit">Submit</button>
    </form>
    
    
    // 服务器端验证 CSRF Token
    app.post('/submit', (req, res) => {
      const { csrfToken } = req.body
      if (!csrfToken || csrfToken !== req.session.csrfToken) {
        return res.status(403).send('Invalid CSRF token')
      }
      // 处理表单提交
      res.send('Form submitted successfully')
    })
  2. SameSite Cookie
    设置 Cookie 的 SameSite 属性为 Lax 或 Strict,可以限制 Cookie 只能在同站请求中发送,减少 CSRF 攻击的风险。Lax 模式允许在顶级导航请求中发送 Cookie,而 Strict 模式则完全禁止跨站请求中的 Cookie 发送。

    Set-Cookie: session=abc123; SameSite=Lax
  3. Referer Check
    检查请求的 Referer 头,确保请求来自合法的来源。如果 Referer 头为空或来自未知来源,可以拒绝请求。

    app.post('/submit', (req, res) => {
      const referer = req.headers.referer
      if (!referer || !referer.startsWith('https://yourdomain.com')) {
        return res.status(403).send('Invalid referer')
      }
      // 处理表单提交
      res.send('Form submitted successfully')
    })
  4. 双重提交 Cookie
    在请求中包含一个与 Cookie 一致的 Token,服务器端验证两个 Token 是否匹配。这种方法可以防止攻击者在跨站请求中伪造 Cookie。

    // 设置 Cookie
    res.cookie('csrfToken', csrfToken, { httpOnly: true })
    
    // 客户端获取 Cookie 并发送请求
    fetch('/submit', {
      method: 'POST',
      headers: {
        'X-CSRF-Token': document.cookie.match(/csrfToken=([^;]+)/)[1]
      },
      body: JSON.stringify({ username: 'john' })
    })
    
    
    // 服务器端验证 CSRF Token
    app.post('/submit', (req, res) => {
      const { 'x-csrf-token': csrfToken } = req.headers
      const cookieToken = req.cookies.csrfToken
      if (!csrfToken || csrfToken !== cookieToken) {
        return res.status(403).send('Invalid CSRF token')
      }
      // 处理表单提交
      res.send('Form submitted successfully')
    })

三、综合安全策略

除了针对 XSS 和 CSRF 的具体措施外,还有一些通用的安全策略可以帮助提高前端应用的整体安全性:

  1. 定期更新依赖库
    及时更新项目中使用的第三方库,修复已知的安全漏洞。可以使用工具如 npm audit 或 yarn audit 来检查和更新依赖库。

    npm audit
    npm update
  2. 使用 HTTPS
    启用 HTTPS 协议,确保数据传输的安全性。HTTPS 可以防止中间人攻击,保护用户的隐私和数据安全。可以通过 Let’s Encrypt 等免费证书服务获取 SSL 证书。

    sudo apt-get install certbot python3-certbot-nginx
    sudo certbot --nginx -d yourdomain.com
  3. 安全审计
    定期进行安全审计,发现并修复潜在的安全问题。可以使用静态代码分析工具如 ESLint、SonarQube 等,以及动态扫描工具如 OWASP ZAP、Burp Suite 等。

    npx eslint .
  4. 用户教育
    教育用户不要点击不明链接,不要在不安全的网络环境中登录账户。可以通过用户手册、帮助文档等方式向用户普及安全知识。

  5. 日志记录和监控
    记录关键操作的日志,并设置监控报警,及时发现和响应异常行为。可以使用 ELK Stack(Elasticsearch, Logstash, Kibana)等工具进行日志管理和分析。

    sudo apt-get install elasticsearch logstash kibana
  6. 权限管理
    实施最小权限原则,确保每个用户和组件只拥有完成任务所需的最低权限。使用 RBAC(基于角色的访问控制)或 ABAC(基于属性的访问控制)等权限管理模型。

  7. 数据加密
    对敏感数据进行加密存储和传输,防止数据泄露。可以使用 AES、RSA 等加密算法,以及 OpenSSL 等工具进行数据加密。

    const crypto = require('crypto')
    
    function encrypt(text, key) {
      const iv = crypto.randomBytes(16)
      const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(key), iv)
      let encrypted = cipher.update(text)
      encrypted = Buffer.concat([encrypted, cipher.final()])
      return iv.toString('hex') + ':' + encrypted.toString('hex')
    }
    
    function decrypt(encryptedText, key) {
      const textParts = encryptedText.split(':')
      const iv = Buffer.from(textParts.shift(), 'hex')
      const encryptedTextBuffer = Buffer.from(textParts.join(':'), 'hex')
      const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(key), iv)
      let decrypted = decipher.update(encryptedTextBuffer)
      decrypted = Buffer.concat([decrypted, decipher.final()])
      return decrypted.toString()
    }
    
    const key = '0123456789abcdef0123456789abcdef' // 32 字节密钥
    const text = 'Hello, World!'
    const encryptedText = encrypt(text, key)
    console.log('Encrypted:', encryptedText)
    const decryptedText = decrypt(encryptedText, key)
    console.log('Decrypted:', decryptedText)

四、实战案例

为了更好地理解如何在实际项目中应用这些安全措施,我们来看一个简单的示例。假设我们有一个评论系统,用户可以发表评论,我们需要防止 XSS 和 CSRF 攻击。

4.1 项目结构

/comment-system
│── /public
│   ├── index.html
│   └── script.js
├── /server
│   ├── app.js
│   └── routes.js
└── package.json

4.2 客户端代码

<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Comment System</title>
    <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://trusted.cdn.com; style-src 'self' 'unsafe-inline' https://trusted.cdn.com">
</head>
<body>
    <h1>Comment System</h1>
    <form id="comment-form">
        <input type="text" id="comment" placeholder="Enter your comment">
        <button type="submit">Submit</button>
    </form>
    <div id="comments"></div>
    <script src="script.js"></script>
</body>
</html>


// public/script.js
document.addEventListener('DOMContentLoaded', () => {
    const form = document.getElementById('comment-form');
    const commentInput = document.getElementById('comment');
    const commentsDiv = document.getElementById('comments');

    form.addEventListener('submit', async (event) => {
        event.preventDefault();

        const comment = escapeHtml(commentInput.value);
        const response = await fetch('/api/comments', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'X-CSRF-Token': getCsrfToken()
            },
            body: JSON.stringify({ comment })
        });

        if (response.ok) {
            commentInput.value = '';
            loadComments();
        } else {
            alert('Failed to submit comment');
        }
    });

    function loadComments() {
        fetch('/api/comments')
            .then(response => response.json())
            .then(comments => {
                commentsDiv.innerHTML = comments.map(comment => `<p>${escapeHtml(comment)}</p>`).join('');
            });
    }

    function escapeHtml(unsafe) {
        return unsafe
            .replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;")
            .replace(/"/g, "&quot;")
            .replace(/'/g, "&#039;");
    }

    function getCsrfToken() {
        return document.cookie.match(/csrfToken=([^;]+)/)?.[1] || '';
    }

    loadComments();
});

4.3 服务器端代码

// server/app.js
const express = require('express');
const cookieParser = require('cookie-parser');
const helmet = require('helmet');
const session = require('express-session');
const bodyParser = require('body-parser');
const routes = require('./routes');

const app = express();

app.use(helmet());
app.use(cookieParser());
app.use(session({
    secret: 'your-secret-key',
    resave: false,
    saveUninitialized: true,
    cookie: { secure: true, httpOnly: true, sameSite: 'lax' }
}));
app.use(bodyParser.json());

app.use('/api', routes);

app.listen(3000, () => {
    console.log('Server is running on port 3000');
});


// server/routes.js
const express = require('express');
const router = express.Router();
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });

router.use(csrfProtection);

router.get('/comments', (req, res) => {
    // 模拟从数据库获取评论
    const comments = ['First comment', 'Second comment'];
    res.json(comments);
});

router.post('/comments', (req, res) => {
    const { comment } = req.body;
    const { csrfToken } = req.body;

    if (!csrfToken || csrfToken !== req.csrfToken()) {
        return res.status(403).send('Invalid CSRF token');
    }

    // 模拟保存评论到数据库
    console.log('New comment:', comment);
    res.send('Comment submitted successfully');
});

module.exports = router;

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
javascript2025-03-05
JS构造函数里的属性方法与原型上的属性方法的区别
1\. 把方法写在原型上比写在构造函数里消耗内存更小,因为一个类的原型只有一个,写在原型上的方法可以被所有实例共享,实例化的时候并不会在实例中再复制一份; 2\. 写在构造函数里的方法,在实例化的时候,会在每一个实例对象上面复制一份,所以消耗的内存更高,因此我们一般把属性定义在构造函数里,把方法定义在原型上 3\. 在构造函数里定义的属性方法要比原型中的优先级更高,如果定义了同名的属性或方法,则构造函数里的会覆盖原型上面的 注意: 定义...
学习面试面试官
javascript2025-03-04
JavaScript 并发模型与事件循环:单线程的异步魔法
1\. JavaScript 的单线程特性 JavaScript 是单线程的,这意味着它一次只能执行一个任务。这种设计简化了编程模型,避免了多线程环境中的复杂问题(如死锁、竞态条件等)。然而,单线程也意味着如果某个任务耗时较长,可能会阻塞后续任务的执行。 为了解决这个问题,JavaScript 引入了 事件循环 和 非阻塞 I/O 机制,使得它能够在执行同步代码的同时,处理异步任务(如定时器、网络请求、I/O 操作等)。 2\. (Ev...
学习面试面试官
八股文2025-03-03
什么是Polyfill?在Node.js中如何使用Polyfill
在 中,面试官经常会问一些关于浏览器兼容性和代码适应性的问题。其中,“什么是Polyfill?在Node.js中如何使用Polyfill?”这一题目就是一个典型实例。本文将详尽介绍Polyfill的概念、使用场景,并通过具体示例代码展示如何在Node.js中使用Polyfill。 什么是Polyfill? Polyfill是一种代码或插件,通过为浏览器提供尚未支持的原生功能,使得现代JavaScript特性可以在旧版浏览器或JavaSc...
学习面试面试官
typescript2025-02-26
TypeScript 中的 interface和 type 类型有什么区别?它们各自有什么用途?
在 TypeScript 中, 和 都可以用于定义类型,但它们有一些细微的差异和特定的应用场景。下面我们详细比较它们的 区别 、 相同点 以及 各自的用途 。 一、相同点 1. 描述对象的结构 : 和 都可以用来描述对象的结构。 2. 都支持可选属性和只读属性 : 3. 都可以扩展 (类似于继承)。不过扩展的方式不同(具体见下文)。 二、区别 | 特性 | interface | type | | | | | | 定义方式 | 使用 关...
学习面试面试官
javascript2024-09-18
面试官:sessionStorage 能在多个标签页之间共享数据吗?
sessionStorage 能在多个标签页之间共享数据吗?在回答这个问题之前我们先来聊聊另一个存储API localstorage localstorage 与 sessionStorage 经常是放在一起讨论,那他们之间的区别是什么呢? 只读的 属性允许你访问一个 源(origin)的对象 ;存储的数据将保存在浏览器会话中。 类似 ,但其区别在于:存储在 的数据可以长期保留;而当页面会话结束——也就是说,当页面被关闭时,存储在 的数...
学习面试面试官
浏览器2023-11-29
浏览器输入url会发生什么 - 巨详细完整版
当我们向浏览器的地址栏输入URL的时候,网络会进行一系列的操作,最终获取到我们所需要的文件,如何交给浏览器进行渲染 我们所关注的问题也就是: 如何获取到我们所需要的文件 浏览器是如何渲染的 大致的执行顺序 URL解析 DNS 解析:缓存判断 + 查询IP地址 TCP 连接:TCP 三次握手 SSL/TLS四次握手(只有https才有这一步) 浏览器发送请求 服务器响应请求并返回数据 浏览器解析渲染页面 断开连接:TCP 四次挥手 URL...
学习面试面试官

评论

请登录后发表评论

去登录
加载评论中...

目录