Web 安全:跨域、XSS 攻击与 CSRF 攻击

Web 安全:跨域、XSS 攻击与 CSRF 攻击

面试必备知识点 + 实战示例代码


目录

  • [一、跨域 (CORS)](#一、跨域 (CORS) "#%E4%B8%80%E8%B7%A8%E5%9F%9F-cors")
  • [二、XSS 攻击](#二、XSS 攻击 "#%E4%BA%8Cxss-%E6%94%BB%E5%87%BB")
  • [三、CSRF 攻击](#三、CSRF 攻击 "#%E4%B8%89csrf-%E6%94%BB%E5%87%BB")
  • 四、安全最佳实践

一、跨域 (CORS)

1.1 什么是跨域?

同源策略 是浏览器的一项核心安全机制,限制一个源的文档或脚本如何能与另一个源的资源进行交互。

同源判断: 三个条件全部相同才算同源

makefile 复制代码
http://www.example.com:80/path

协议:   http
域名:   www.example.com
端口:   80

跨域示例:

当前页面 目标URL 是否跨域 原因
http://a.com http://a.com/api 同源
http://a.com https://a.com/api 协议不同
http://a.com http://b.com/api 域名不同
http://a.com:80 http://a.com:8080 端口不同
http://a.com http://sub.a.com 子域名不同

1.2 跨域解决方案

方案一:CORS (跨域资源共享) - 推荐

服务端设置响应头:

javascript 复制代码
// Node.js / Express
app.use((req, res, next) => {
  // 允许所有源 (生产环境慎用)
  res.header('Access-Control-Allow-Origin', '*')

  // 允许指定源
  // res.header('Access-Control-Allow-Origin', 'https://example.com')

  // 允许的请求方法
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')

  // 允许的请求头
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization')

  // 允许携带凭证
  res.header('Access-Control-Allow-Credentials', 'true')

  // 预检请求缓存时间 (秒)
  res.header('Access-Control-Max-Age', '86400')

  // 处理 OPTIONS 预检请求
  if (req.method === 'OPTIONS') {
    return res.sendStatus(200)
  }
  next()
})

简单请求 vs 预检请求:

diff 复制代码
简单请求条件:
- 方法:GET、HEAD、POST
- 头部:Accept、Accept-Language、Content-Language、Content-Type
- Content-Type: application/x-www-form-urlencoded、multipart/form-data、text/plain

预检请求 (OPTIONS):
- 不满足简单请求条件时自动触发
- 先发 OPTIONS 请求询问是否允许
- 允许后才发送实际请求
方案二:JSONP (已过时)
html 复制代码
<script>
  // 定义回调函数
  function handleResponse(data) {
    console.log('收到数据:', data)
  }
</script>

<!-- 通过 script 标签跨域请求 -->
<script src="http://api.example.com/data?callback=handleResponse"></script>
javascript 复制代码
// 服务端返回
handleResponse({ name: "张三", age: 25 })

缺点: 只支持 GET 请求,存在安全风险,已不推荐使用。

方案三:代理服务器
javascript 复制代码
// vite.config.js
export default {
  server: {
    proxy: {
      '/api': {
        target: 'http://backend-api.com',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  }
}

// nginx 配置
location /api {
  proxy_pass http://backend-api.com;
  proxy_set_header Host $host;
}
方案四:WebSocket (不受同源策略限制)
javascript 复制代码
const ws = new WebSocket('ws://another-domain.com/chat')
ws.onopen = () => console.log('连接已建立')
ws.onmessage = (event) => console.log('收到消息:', event.data)

二、XSS 攻击

2.1 什么是 XSS 攻击?

跨站脚本攻击 (Cross-Site Scripting) 是攻击者向网页注入恶意脚本,当用户访问时执行。

2.2 XSS 三种类型

类型一:反射型 XSS (Reflected XSS)

恶意脚本通过 URL 参数反射给用户。

xml 复制代码
攻击链接:
http://example.com/search?q=<script>alert(document.cookie)</script>

页面直接渲染参数:
<div>搜索结果: <%= q %></div>
类型二:存储型 XSS (Stored XSS)

恶意脚本被存储到服务器,所有访问该页面的用户都会执行。

javascript 复制代码
// 危险的评论功能
comment = req.body.comment
db.save(comment)  // 直接存储,没有转义

// 渲染时直接输出
<div class="comment">${comment}</div>
类型三:DOM 型 XSS (DOM-based XSS)

恶意代码通过 DOM 操作修改页面执行。

javascript 复制代码
// 危险代码
const hash = location.hash.substring(1)
document.getElementById('output').innerHTML = hash

// 攻击 URL:
// http://example.com/#<img src=x onerror=alert(1)>

2.3 XSS 攻击危害

  • 窃取用户 Cookie (Session 劫持)
  • 篡改网页内容
  • 重定向到钓鱼网站
  • 监听用户键盘操作
  • 执行恶意操作

2.4 XSS 防御措施

措施一:输出转义 - 最重要
javascript 复制代码
// HTML 转义函数
function escapeHtml(unsafe) {
  return unsafe
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;")
}

// 使用
div.innerHTML = escapeHtml(userInput)
措施二:使用 CSP (Content Security Policy)
html 复制代码
<!-- 通过 HTTP 头或 meta 标签设置 -->
<meta http-equiv="Content-Security-Policy" content="
  default-src 'self';
  script-src 'self' 'unsafe-inline' https://trusted.cdn.com;
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  connect-src 'self' api.example.com;
">
nginx 复制代码
# nginx 配置
add_header Content-Security-Policy "default-src 'self'; script-src 'self'";
add_header X-Content-Type-Options "nosniff";
add_header X-Frame-Options "DENY";
add_header X-XSS-Protection "1; mode=block";

什么是 HttpOnly?

HttpOnly 是 Cookie 的一个属性,设置为 true 后,JavaScript 无法通过 document.cookie 读取该 Cookie,从而有效防止 XSS 攻击窃取 Session ID。

javascript 复制代码
// 服务端设置 Cookie 时添加 HttpOnly
res.cookie('sessionId', sessionId, {
  httpOnly: true,  // JavaScript 无法读取
  secure: true,    // 仅 HTTPS 传输
  sameSite: 'strict'  // 防止 CSRF
})

HttpOnly 作用演示:

javascript 复制代码
// 服务端设置
res.cookie('sessionId', 'abc123', { httpOnly: true })
res.cookie('userInfo', JSON.stringify({ name: 'Alice' }))  // 没有 httpOnly

// 前端 JavaScript
console.log(document.cookie)
// 输出: userInfo={"name":"Alice"}
// sessionId 不会显示!

Cookie 属性完整说明:

属性 作用 推荐值
httpOnly 防止 JS 读取 Cookie true (敏感 Cookie)
secure 仅通过 HTTPS 传输 true (生产环境)
sameSite 防止 CSRF 攻击 'strict''lax'
domain 指定 Cookie 有效域 不设置或当前域
path 指定 Cookie 有效路径 /
maxAge Cookie 存活时间(毫秒) 根据业务设置
expires Cookie 过期日期 -

不同后端框架设置示例:

javascript 复制代码
// Express / Node.js
res.cookie('sessionId', token, {
  httpOnly: true,
  secure: process.env.NODE_ENV === 'production',
  sameSite: 'strict',
  maxAge: 24 * 60 * 60 * 1000
})

// koa
ctx.cookies.set('sessionId', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict'
})

// PHP
setcookie('sessionId', $token, [
  'expires' => time() + 86400,
  'path' => '/',
  'domain' => 'example.com',
  'secure' => true,
  'httponly' => true,
  'samesite' => 'Strict'
])

// Java (Spring Boot)
ResponseCookie cookie = ResponseCookie.from("sessionId", token)
    .httpOnly(true)
    .secure(true)
    .sameSite("Strict")
    .maxAge(24 * 60 * 60)
    .build();

设置响应头方式:

nginx 复制代码
# nginx 设置 HttpOnly Cookie
add_header Set-Cookie "sessionId=abc123; HttpOnly; Secure; SameSite=Strict";
javascript 复制代码
// 原生 HTTP 响应头
res.setHeader('Set-Cookie', [
  'sessionId=abc123; HttpOnly; Secure; SameSite=Strict; Max-Age=86400; Path=/'
])
措施四:避免使用 innerHTML
javascript 复制代码
// 危险
element.innerHTML = userInput

// 安全
element.textContent = userInput
element.innerText = userInput

// 动态创建元素
const div = document.createElement('div')
div.textContent = userInput
element.appendChild(div)
措施五:输入验证
javascript 复制代码
// 白名单验证
function sanitizeInput(input) {
  // 只允许字母、数字、空格
  return input.replace(/[^a-zA-Z0-9\s]/g, '')
}

三、CSRF 攻击

3.1 什么是 CSRF 攻击?

跨站请求伪造 (Cross-Site Request Forgery) 是攻击者诱导用户在已登录的状态下,向目标网站发送非本意的请求。

3.2 CSRF 攻击原理

xml 复制代码
用户已登录 bank.com (持有有效 Cookie)

1. 用户访问恶意网站 evil.com
2. evil.com 页面包含:
   <form action="https://bank.com/transfer" method="POST">
     <input name="to" value="attacker">
     <input name="amount" value="10000">
   </form>
   <script>document.forms[0].submit()</script>

3. 浏览器自动携带 bank.com 的 Cookie
4. 银行服务器认为是用户本人操作
5. 转账成功!

3.3 XSS vs CSRF 对比

特性 XSS CSRF
攻击者 在目标网站注入脚本 伪造用户请求
执行位置 用户浏览器 用户浏览器
需要 XSS
需要 Cookie 获取 否 (自动携带)
防御重点 输入转义、CSP Token 验证

3.4 CSRF 防御措施

措施一:CSRF Token - 最有效
javascript 复制代码
// 服务端生成 Token
const crypto = require('crypto')
function generateToken() {
  return crypto.randomBytes(32).toString('hex')
}

// 存储到 Session
req.session.csrfToken = generateToken()

// 渲染表单时注入
<form method="POST" action="/transfer">
  <input type="hidden" name="csrf_token" value="<%= csrfToken %>">
  <input name="to" value="">
  <input name="amount" value="">
</form>

// 验证 Token
app.post('/transfer', (req, res) => {
  if (req.body.csrf_token !== req.session.csrfToken) {
    return res.status(403).send('CSRF Token 验证失败')
  }
  // 处理转账...
})
javascript 复制代码
// Strict: 完全禁止第三方 Cookie
res.cookie('sessionId', sessionId, {
  sameSite: 'strict'
})

// Lax: 允许部分 GET 请求
res.cookie('sessionId', sessionId, {
  sameSite: 'lax'
})
vbnet 复制代码
SameSite 属性说明:

Strict:  最严格,任何跨站请求都不发送 Cookie
Lax:     允许顶级导航 GET 请求发送 Cookie(默认)
None:    必须配合 Secure,允许跨站发送
措施三:验证 Referer / Origin
javascript 复制代码
app.post('/transfer', (req, res) => {
  const origin = req.headers.origin
  const referer = req.headers.referer

  const allowedOrigins = ['https://bank.com', 'https://www.bank.com']

  if (!allowedOrigins.includes(origin) && !referer?.startsWith('https://bank.com')) {
    return res.status(403).send('非法请求来源')
  }

  // 处理业务...
})
javascript 复制代码
// 1. 设置 CSRF Cookie
res.cookie('csrfToken', generateToken(), {
  httpOnly: false,  // 需要能被 JS 读取
  sameSite: 'strict'
})

// 2. 前端请求时从 Cookie 读取并放入 Header
// axios 拦截器
axios.interceptors.request.use(config => {
  const csrfToken = getCookie('csrfToken')
  config.headers['X-CSRF-Token'] = csrfToken
  return config
})

// 3. 服务端验证
app.use((req, res, next) => {
  const cookieToken = req.cookies.csrfToken
  const headerToken = req.headers['x-csrf-token']

  if (cookieToken && cookieToken !== headerToken) {
    return res.status(403).send('CSRF 验证失败')
  }
  next()
})
措施五:重要操作二次确认
javascript 复制代码
// 转账前要求输入密码或验证码
app.post('/transfer', (req, res) => {
  const { password, amount, to } = req.body

  // 验证支付密码
  if (!verifyPaymentPassword(req.user.id, password)) {
    return res.status(400).send('支付密码错误')
  }

  // 执行转账
  executeTransfer(req.user.id, to, amount)
})

四、安全最佳实践

4.1 安全检查清单

  • 所有用户输入都经过转义/验证
  • 敏感 Cookie 设置 HttpOnly、Secure、SameSite
  • 实施 CSP 策略
  • 重要操作使用 CSRF Token
  • 验证请求的 Referer/Origin
  • HTTPS 强制跳转
  • 实施速率限制防止暴力攻击
  • 敏感操作二次确认
  • 定期安全审计和依赖更新

4.2 安全 HTTP 响应头

javascript 复制代码
// Express 中间件示例
app.use(helmet())  // npm install helmet

// 或手动设置
app.use((req, res, next) => {
  // 防止点击劫持
  res.setHeader('X-Frame-Options', 'DENY')

  // 防止 MIME 类型嗅探
  res.setHeader('X-Content-Type-Options', 'nosniff')

  // 启用浏览器 XSS 过滤
  res.setHeader('X-XSS-Protection', '1; mode=block')

  // 限制引用来源
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin')

  // 权限策略
  res.setHeader('Permissions-Policy', 'geolocation=(), microphone=()')

  next()
})

4.3 面试常问问题

Q: 跨域是安全问题吗?

A: 跨域不是安全问题,它是浏览器的同源策略保护。CORS 是用来安全地放松这个限制的机制。

Q: XSS 和 CSRF 的区别?

A: XSS 是注入恶意代码,在用户浏览器执行;CSRF 是利用用户身份伪造请求。XSS 可以获取 Cookie,CSRF 自动携带 Cookie。

Q: 为什么 Token 可以防御 CSRF?

A: 攻击者无法获取用户页面中的 CSRF Token(同源策略保护),所以无法构造合法的请求。

Q: Cookie 的 SameSite 属性?

A: Strict 完全禁止跨站 Cookie,Lax 允许部分 GET 请求,None 必须配合 Secure 允许跨站。

Q: 什么是 HttpOnly Cookie?作用是什么?

A: HttpOnly 是 Cookie 的安全属性,设置为 true 后 JavaScript 无法通过 document.cookie 读取该 Cookie。主要作用是防止 XSS 攻击窃取 Session ID。

Q: HttpOnly 能完全防御 XSS 攻击吗?

A: 不能。HttpOnly 只能防止 Cookie 被窃取,但 XSS 攻击仍可执行其他恶意操作,如篡改页面内容、重定向、监听键盘等。需要配合 CSP、输出转义等综合防御。

Q: Cookie 中 httpOnly、secure、sameSite 的区别?

A:

  • httpOnly: 防止 JavaScript 读取 Cookie,防御 XSS
  • secure: 只在 HTTPS 下传输,防止中间人攻击
  • sameSite: 防止跨站请求发送 Cookie,防御 CSRF

示例代码

文件夹结构

bash 复制代码
examples/
├── package.json          # 项目依赖配置
├── cors-demo.js          # 跨域 (CORS) 演示
├── xss-demo.html         # XSS 攻击交互演示
├── xss-defense.js        # XSS 防御工具函数
├── csrf-demo.js          # CSRF 攻击与防御演示
└── httponly-demo.js      # HttpOnly Cookie 演示

1. package.json

json 复制代码
{
  "name": "web-security-examples",
  "version": "1.0.0",
  "description": "Web 安全示例:跨域、XSS 攻击、CSRF 攻击演示与防御",
  "scripts": {
    "cors": "node cors-demo.js",
    "csrf": "node csrf-demo.js",
    "defense": "node xss-defense.js",
    "httponly": "node httponly-demo.js"
  },
  "dependencies": {
    "cookie-parser": "^1.4.6",
    "cors": "^2.8.5",
    "express": "^4.18.2",
    "express-session": "^1.17.3",
    "helmet": "^7.1.0"
  }
}

2. cors-demo.js - 跨域演示

javascript 复制代码
/**
 * 跨域演示代码
 *
 * 运行方式:
 * 1. npm install express cors
 * 2. node cors-demo.js
 * 3. 访问 http://localhost:3000
 */

const express = require('express')
const cors = require('cors')

const app = express()

// ============================================
// 方式一:使用 cors 中间件(推荐)
// ============================================

app.use(cors({
  origin: ['http://localhost:3000', 'https://example.com'],
  credentials: true,
  optionsSuccessStatus: 200
}))

// ============================================
// 方式二:手动配置 CORS
// ============================================

/*
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', '*')
  res.header('Access-Control-Allow-Credentials', 'true')
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS')
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With')
  res.header('Access-Control-Expose-Headers', 'X-Total-Count, X-Request-ID')
  res.header('Access-Control-Max-Age', '86400')

  if (req.method === 'OPTIONS') {
    return res.sendStatus(204)
  }
  next()
})
*/

// API 路由
app.get('/', (req, res) => {
  res.send(`
    <!DOCTYPE html>
    <html>
    <head><title>CORS 测试</title></head>
    <body>
      <h1>CORS 跨域测试</h1>
      <button onclick="testSimpleRequest()">简单请求 (GET)</button>
      <button onclick="testComplexRequest()">复杂请求 (PUT)</button>
      <script>
        async function testSimpleRequest() {
          const res = await fetch('/api/data');
          const data = await res.json();
          console.log(data);
        }
        async function testComplexRequest() {
          const res = await fetch('/api/data', {
            method: 'PUT',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ name: 'test' })
          });
          const data = await res.json();
          console.log(data);
        }
      </script>
    </body>
    </html>
  `);
});

app.get('/api/data', (req, res) => {
  res.json({ message: 'GET 请求成功', timestamp: new Date().toISOString() });
});

app.listen(3000, () => console.log('服务器运行在 http://localhost:3000'))

3. xss-demo.html - XSS 攻击演示

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>XSS 攻击演示与防御</title>
  <style>
    body { font-family: Arial; max-width: 1000px; margin: 20px auto; padding: 20px; }
    .vulnerable { border: 2px solid #f44336; background: #ffebee; padding: 15px; margin: 10px 0; }
    .safe { border: 2px solid #4CAF50; background: #e8f5e9; padding: 15px; margin: 10px 0; }
    button { padding: 10px 20px; margin: 5px; cursor: pointer; }
    .btn-attack { background: #f44336; color: white; }
    .btn-safe { background: #4CAF50; color: white; }
  </style>
</head>
<body>
  <h1>XSS 攻击演示与防御</h1>

  <!-- 反射型 XSS -->
  <div class="vulnerable">
    <h3>危险示例 - innerHTML (可触发 XSS)</h3>
    <input type="text" id="dangerous-input" placeholder="输入: &lt;img src=x onerror=alert(1)&gt;">
    <button class="btn-attack" onclick="testDangerous()">测试攻击</button>
    <div id="dangerous-output">结果将显示在这里...</div>
  </div>

  <div class="safe">
    <h3>安全示例 - textContent</h3>
    <input type="text" id="safe-input" placeholder="输入相同内容测试">
    <button class="btn-safe" onclick="testSafe()">测试安全</button>
    <div id="safe-output">结果将显示在这里...</div>
  </div>

  <script>
    // 危险:直接使用 innerHTML
    function testDangerous() {
      const input = document.getElementById('dangerous-input').value;
      document.getElementById('dangerous-output').innerHTML = input;
    }

    // 安全:使用 textContent
    function testSafe() {
      const input = document.getElementById('safe-input').value;
      document.getElementById('safe-output').textContent = input;
    }

    // HTML 转义函数
    function escapeHtml(unsafe) {
      return unsafe
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#039;");
    }
  </script>
</body>
</html>

4. xss-defense.js - XSS 防御工具

javascript 复制代码
/**
 * XSS 防御工具函数
 */

// HTML 转义
function escapeHtml(unsafe) {
  if (typeof unsafe !== 'string') return unsafe
  return unsafe
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;")
    .replace(/\//g, "&#x2F;")
}

// JavaScript 转义
function escapeJs(unsafe) {
  if (typeof unsafe !== 'string') return unsafe
  return unsafe
    .replace(/\\/g, "\\\\")
    .replace(/'/g, "\\'")
    .replace(/"/g, '\\"')
    .replace(/\n/g, "\\n")
    .replace(/\r/g, "\\r")
}

// URL 转义
function escapeUrl(unsafe) {
  return encodeURIComponent(unsafe)
}

// CSP Header 生成器
function generateCSP(options = {}) {
  const {
    defaultSrc = ["'self'"],
    scriptSrc = ["'self'"],
    styleSrc = ["'self'", "'unsafe-inline'"],
    imgSrc = ["'self'", "data:", "https:"],
  } = options

  return [
    `default-src ${defaultSrc.join(' ')}`,
    `script-src ${scriptSrc.join(' ')}`,
    `style-src ${styleSrc.join(' ')}`,
    `img-src ${imgSrc.join(' ')}`,
  ].join('; ')
}

// 安全 Cookie 选项
function getSecureCookieOptions(options = {}) {
  return {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    path: '/',
    maxAge: 24 * 60 * 60 * 1000,
    ...options
  }
}

// 安全响应头
function getSecurityHeaders() {
  return {
    'X-Frame-Options': 'DENY',
    'X-Content-Type-Options': 'nosniff',
    'X-XSS-Protection': '1; mode=block',
    'Referrer-Policy': 'strict-origin-when-cross-origin',
    'Permissions-Policy': 'geolocation=(), microphone=(), camera=()',
    'Content-Security-Policy': generateCSP()
  }
}

module.exports = {
  escapeHtml,
  escapeJs,
  escapeUrl,
  generateCSP,
  getSecureCookieOptions,
  getSecurityHeaders
}

5. csrf-demo.js - CSRF 攻击与防御演示

javascript 复制代码
/**
 * CSRF 攻击与防御演示
 *
 * 运行方式:
 * 1. npm install express express-session cookie-parser
 * 2. node csrf-demo.js
 */

const express = require('express')
const session = require('express-session')
const cookieParser = require('cookie-parser')
const crypto = require('crypto')

const app = express()
app.use(express.urlencoded({ extended: true }))
app.use(express.json())
app.use(cookieParser())
app.use(session({
  secret: 'my-secret-key',
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,
    secure: false,
    sameSite: 'strict',
    maxAge: 24 * 60 * 60 * 1000
  }
}))

// 模拟数据库
const db = {
  users: [
    { id: 1, username: 'alice', balance: 10000 }
  ],
  transfers: []
}

// 生成 CSRF Token
function generateCSRFToken() {
  return crypto.randomBytes(32).toString('hex')
}

// CSRF 验证中间件
function csrfProtection(req, res, next) {
  if (req.method === 'GET') {
    if (!req.session.csrfToken) {
      req.session.csrfToken = generateCSRFToken()
    }
    return next()
  }

  const token = req.body.csrf_token || req.headers['x-csrf-token']
  const sessionToken = req.session.csrfToken

  if (!token || token !== sessionToken) {
    return res.status(403).send('CSRF Token 验证失败')
  }

  req.session.csrfToken = generateCSRFToken()
  next()
}

// 主页
app.get('/', (req, res) => {
  const user = req.session.user
  res.send(`
    <!DOCTYPE html>
    <html>
    <head><title>CSRF 演示</title></head>
    <body>
      <h1>CSRF 攻击与防御演示</h1>
      ${user ? `<p>已登录: ${user.username}, 余额: ¥${user.balance}</p>` : '<p>请先登录</p>'}

      ${!user ? `
        <form method="POST" action="/login">
          <input name="username" value="alice" required>
          <input name="password" value="123456" required>
          <button type="submit">登录</button>
        </form>
      ` : `
        <!-- 无保护的转账 -->
        <h3>无保护的转账 (存在 CSRF 漏洞)</h3>
        <form method="POST" action="/transfer/vulnerable">
          <input name="to" value="attacker">
          <input name="amount" value="100">
          <button type="submit">转账 (无保护)</button>
        </form>

        <!-- 受保护的转账 -->
        <h3>受保护的转账 (CSRF Token)</h3>
        <form method="POST" action="/transfer/protected">
          <input type="hidden" name="csrf_token" value="${req.session.csrfToken || ''}">
          <input name="to" placeholder="收款人">
          <input name="amount" placeholder="金额">
          <button type="submit">转账 (受保护)</button>
        </form>

        <a href="/logout">退出登录</a>
      `}
    </body>
    </html>
  `)
})

// 登录
app.post('/login', (req, res) => {
  const { username, password } = req.body
  if (username === 'alice' && password === '123456') {
    req.session.user = db.users[0]
    req.session.csrfToken = generateCSRFToken()
    res.redirect('/')
  } else {
    res.send('登录失败')
  }
})

// 退出
app.get('/logout', (req, res) => {
  req.session.destroy()
  res.redirect('/')
})

// 无保护的转账
app.post('/transfer/vulnerable', (req, res) => {
  const user = req.session.user
  if (!user) return res.status(401).send('请先登录')

  const { to, amount } = req.body
  user.balance -= parseInt(amount)

  res.send(`<p>向 ${to} 转账 ¥${amount} 成功!余额: ¥${user.balance}</p>`)
})

// 受保护的转账
app.post('/transfer/protected', csrfProtection, (req, res) => {
  const user = req.session.user
  if (!user) return res.status(401).send('请先登录')

  const { to, amount } = req.body
  user.balance -= parseInt(amount)

  res.send(`<p>向 ${to} 转账 ¥${amount} 成功!余额: ¥${user.balance}</p>`)
})

app.listen(3000, () => console.log('CSRF 演示服务器运行在 http://localhost:3000'))
javascript 复制代码
/**
 * HttpOnly Cookie 演示
 *
 * 运行方式:
 * 1. npm install express cookie-parser
 * 2. node httponly-demo.js
 */

const express = require('express')
const cookieParser = require('cookie-parser')

const app = express()
app.use(cookieParser())
app.use(express.urlencoded({ extended: true }))

const users = [
  { id: 1, username: 'alice', password: '123456' }
]

const sessions = new Map()

// Session 中间件
app.use((req, res, next) => {
  const sessionId = req.cookies.session_id
  if (sessionId && sessions.has(sessionId)) {
    req.user = users.find(u => u.id === sessions.get(sessionId).userId)
  }
  next()
})

// 主页
app.get('/', (req, res) => {
  res.send(`
    <!DOCTYPE html>
    <html>
    <head><title>HttpOnly Cookie 演示</title></head>
    <body>
      <h1>HttpOnly Cookie 安全演示</h1>
      ${req.user ? `<p>已登录: ${req.user.username}</p>` : '<p>请先登录</p>'}

      ${!req.user ? `
        <form method="POST" action="/login">
          <input name="username" value="alice" required>
          <input name="password" value="123456" required>
          <button type="submit">登录</button>
        </form>
      ` : `
        <h3>JavaScript 读取 Cookie 测试</h3>
        <button onclick="readCookie()">读取 Cookie</button>
        <pre id="cookie-result"></pre>

        <a href="/logout">退出登录</a>
      `}
      <script>
        function readCookie() {
          const cookies = document.cookie;
          document.getElementById('cookie-result').textContent =
            cookies || '(空 - HttpOnly Cookie 无法被 JS 读取!)';
        }
      </script>
    </body>
    </html>
  `)
})

// 登录 - 设置 HttpOnly Cookie
app.post('/login', (req, res) => {
  const { username, password } = req.body
  const user = users.find(u => u.username === username && u.password === password)

  if (!user) return res.send('登录失败')

  const sessionId = Buffer.from(`${user.id}-${Date.now()}`).toString('base64')
  sessions.set(sessionId, { userId: user.id, createdAt: Date.now() })

  // HttpOnly Cookie - JS 无法读取
  res.cookie('session_id', sessionId, {
    httpOnly: true,
    secure: false,
    sameSite: 'strict',
    maxAge: 24 * 60 * 60 * 1000
  })

  // 普通 Cookie - JS 可以读取
  res.cookie('user_info', JSON.stringify({ username: user.username }), {
    maxAge: 24 * 60 * 60 * 1000
  })

  res.redirect('/')
})

app.get('/logout', (req, res) => {
  res.clearCookie('session_id')
  res.clearCookie('user_info')
  res.redirect('/')
})

app.listen(3002, () => console.log('HttpOnly 演示服务器运行在 http://localhost:3002'))

运行方式

bash 复制代码
cd examples
npm install

# 运行各个演示
npm run cors      # http://localhost:3000
npm run defense   # http://localhost:3001
npm run csrf      # http://localhost:3000
npm run httponly  # http://localhost:3002

💡 提示:安全是持续的过程,不是一次性配置。保持警惕,定期审计!

相关推荐
网络点点滴3 小时前
前端与后端的区别与联系
前端
EnCi Zheng3 小时前
M5-markconv自定义CSS样式指南 [特殊字符]
前端·css·python
kyriewen3 小时前
你的网页慢,用户不说直接走——前端性能监控教你“读心术”
前端·性能优化·监控
广州华水科技3 小时前
北斗GNSS变形监测在大坝安全监测中的应用与优势分析
前端
前端老石人4 小时前
前端开发中的 URL 完全指南
开发语言·前端·javascript·css·html
CAE虚拟与现实4 小时前
五一假期闲来无事,来个前段、后端的说明吧
前端·后端·vtk·three.js·前后端
Sarvartha4 小时前
三目运算符
linux·服务器·前端
晓晨的博客4 小时前
ROS1录制的bag包转换为ROS2格式
前端·chrome
Wect4 小时前
LeetCode 72. 编辑距离:动态规划经典题解
前端·算法·typescript
donecoding4 小时前
别再让 pnpm 跟着 nvm 跑了!独立安装终极指南
前端·node.js·前端工程化