Web安全

Web 安全是前端开发不可忽视的重要领域。了解常见的安全威胁和防护措施,是每个开发者的必修课。

常见安全威胁

威胁 说明 危害等级
XSS 跨站脚本攻击
CSRF 跨站请求伪造
SQL 注入 数据库攻击
点击劫持 界面欺骗
中间人攻击 数据窃取
敏感数据泄露 信息泄露

XSS 攻击防护

XSS(Cross-Site Scripting)是攻击者向网页注入恶意脚本的攻击方式。

XSS 类型

类型 说明 示例
反射型 URL 参数注入 ?name=<script>alert(1)</script>
存储型 存储在服务器 评论、用户信息
DOM 型 客户端注入 innerHTML

攻击示例

// 危险代码 - 不要这样写
document.getElementById('output').innerHTML = userInput

// 攻击者输入
const maliciousInput = '<img src=x onerror="alert(document.cookie)">'

// 结果:恶意脚本执行

防护措施

// 1. 使用 textContent 而非 innerHTML
element.textContent = userInput

// 2. HTML 转义
function escapeHtml(str) {
  const escapeMap = {
    '&': '&amp;',
    '<': '&lt;',
    '>': '&gt;',
    '"': '&quot;',
    "'": '&#x27;',
    '/': '&#x2F;'
  }
  return str.replace(/[&<>"'/]/g, char => escapeMap[char])
}

// 使用
element.innerHTML = escapeHtml(userInput)

// 3. 使用 DOMPurify 库
import DOMPurify from 'dompurify'

const clean = DOMPurify.sanitize(dirtyHtml)
element.innerHTML = clean

// 4. Content Security Policy
// HTTP 响应头
// Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'

// 5. HttpOnly Cookie
// 服务端设置
// Set-Cookie: sessionId=abc123; HttpOnly; Secure; SameSite=Strict

React 防护

// React 自动转义
function SafeComponent({ userInput }) {
  return <div>{userInput}</div>  // 自动转义
}

// 危险:dangerouslySetInnerHTML
function DangerousComponent({ html }) {
  return <div dangerouslySetInnerHTML={{ __html: html }} />
}

// 安全使用
import DOMPurify from 'dompurify'

function SafeHtmlComponent({ html }) {
  const clean = DOMPurify.sanitize(html)
  return <div dangerouslySetInnerHTML={{ __html: clean }} />
}

Vue 防护

<template>
  <!-- 自动转义 -->
  <div>{{ userInput }}</div>
  
  <!-- 危险:v-html -->
  <div v-html="html"></div>
</template>

<script>
import DOMPurify from 'dompurify'

export default {
  data() {
    return {
      userInput: '<script>alert(1)</script>'
    }
  },
  computed: {
    safeHtml() {
      return DOMPurify.sanitize(this.html)
    }
  }
}
</script>

CSRF 攻击防护

CSRF(Cross-Site Request Forgery)是攻击者诱导用户在已登录网站上执行非预期操作。

攻击示例

<!-- 恶意网站上的代码 -->
<img src="https://bank.com/transfer?to=attacker&amount=10000">

<!-- 或隐藏表单 -->
<form action="https://bank.com/transfer" method="POST" id="stealForm">
  <input type="hidden" name="to" value="attacker">
  <input type="hidden" name="amount" value="10000">
</form>
<script>
  document.getElementById('stealForm').submit()
</script>

防护措施

// 1. CSRF Token
// 服务端生成 Token
app.get('/form', (req, res) => {
  const csrfToken = generateCSRFToken()
  res.cookie('csrfToken', csrfToken)
  res.render('form', { csrfToken })
})

// 前端提交时携带
const form = document.getElementById('myForm')
const csrfToken = getCookie('csrfToken')

fetch('/api/submit', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'X-CSRF-Token': csrfToken
  },
  body: JSON.stringify(data)
})

// 2. SameSite Cookie
// Set-Cookie: sessionId=abc; SameSite=Strict
// SameSite=Strict  完全禁止跨站发送
// SameSite=Lax     允许安全的跨站请求
// SameSite=None    允许跨站发送(需要 Secure)

// 3. 验证 Referer/Origin
app.post('/api/action', (req, res) => {
  const origin = req.get('Origin') || req.get('Referer')
  
  if (!origin || !origin.startsWith('https://mysite.com')) {
    return res.status(403).json({ error: 'CSRF 验证失败' })
  }
  
  // 处理请求
})

// 4. 双重 Cookie 验证
// 将 Token 同时放在 Cookie 和请求参数中
function submitWithDoubleCookie(data) {
  const token = getCookie('csrfToken')
  
  return fetch('/api/submit', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ ...data, _csrf: token })
  })
}

点击劫持防护

点击劫持是攻击者将目标网站嵌入隐藏的 iframe 中,诱导用户点击。

攻击示例

<!-- 恶意网站 -->
<style>
  iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
  }
  .fake-button {
    position: absolute;
    top: 100px;
    left: 100px;
  }
</style>

<iframe src="https://bank.com/transfer"></iframe>
<div class="fake-button">点击领取奖品</div>

防护措施

// 1. X-Frame-Options 响应头
// X-Frame-Options: DENY           // 禁止嵌入
// X-Frame-Options: SAMEORIGIN     // 只允许同源嵌入
// X-Frame-Options: ALLOW-FROM uri // 允许特定来源

// Express 设置
app.use((req, res, next) => {
  res.setHeader('X-Frame-Options', 'DENY')
  next()
})

// 2. Content-Security-Policy
// Content-Security-Policy: frame-ancestors 'self'

// 3. JavaScript 检测
if (window.top !== window.self) {
  window.top.location = window.self.location
}

// 或
<style>body { display: none; }</style>
<script>
  if (self === top) {
    document.documentElement.style.display = 'block'
  } else {
    top.location = self.location
  }
</script>

安全 HTTP 头

// Express 中设置安全头
const helmet = require('helmet')

app.use(helmet())

// 或手动设置
app.use((req, res, next) => {
  // 防止 MIME 类型嗅探
  res.setHeader('X-Content-Type-Options', 'nosniff')
  
  // XSS 保护
  res.setHeader('X-XSS-Protection', '1; mode=block')
  
  // 禁止嵌入 iframe
  res.setHeader('X-Frame-Options', 'DENY')
  
  // HSTS
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')
  
  // CSP
  res.setHeader('Content-Security-Policy', 
    "default-src 'self'; " +
    "script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net; " +
    "style-src 'self' 'unsafe-inline'; " +
    "img-src 'self' data: https:; " +
    "font-src 'self' https://fonts.gstatic.com; " +
    "connect-src 'self' https://api.db-w.cn"
  )
  
  // Referrer 策略
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin')
  
  // 权限策略
  res.setHeader('Permissions-Policy', 
    'geolocation=(), microphone=(), camera=()'
  )
  
  next()
})

密码安全

// 1. 密码加密存储
const bcrypt = require('bcrypt')

// 加密密码
async function hashPassword(password) {
  const salt = await bcrypt.genSalt(10)
  return bcrypt.hash(password, salt)
}

// 验证密码
async function verifyPassword(password, hash) {
  return bcrypt.compare(password, hash)
}

// 2. 密码强度验证
function validatePassword(password) {
  const rules = {
    minLength: password.length >= 8,
    hasLower: /[a-z]/.test(password),
    hasUpper: /[A-Z]/.test(password),
    hasNumber: /\d/.test(password),
    hasSpecial: /[!@#$%^&*]/.test(password)
  }
  
  return {
    valid: Object.values(rules).every(Boolean),
    rules
  }
}

// 3. 密码输入安全
// 使用 type="password"
<input type="password" name="password" autocomplete="new-password">

// 禁止复制粘贴密码(可选)
<input type="password" oncopy="return false" onpaste="return false">

敏感数据处理

// 1. 不在前端存储敏感数据
// 错误
localStorage.setItem('token', sensitiveToken)
localStorage.setItem('password', password)

// 正确:使用 HttpOnly Cookie 或内存存储
let sessionToken = null  // 内存中,页面刷新后清除

// 2. 敏感信息脱敏
function maskEmail(email) {
  const [name, domain] = email.split('@')
  const maskedName = name.slice(0, 2) + '***'
  return `${maskedName}@${domain}`
}

function maskPhone(phone) {
  return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
}

function maskIdCard(idCard) {
  return idCard.replace(/(\d{4})\d{10}(\d{4})/, '$1**********$2')
}

// 3. 日志安全
// 错误
console.log('用户登录:', { username, password })

// 正确
console.log('用户登录:', { username })

// 生产环境移除 console
if (process.env.NODE_ENV === 'production') {
  console.log = () => {}
  console.debug = () => {}
}

// 4. 错误信息处理
// 错误:暴露敏感信息
app.use((err, req, res, next) => {
  res.status(500).json({ 
    error: err.message,
    stack: err.stack 
  })
})

// 正确:生产环境隐藏详情
app.use((err, req, res, next) => {
  res.status(500).json({
    error: '服务器错误',
    ...(process.env.NODE_ENV === 'development' && {
      message: err.message,
      stack: err.stack
    })
  })
})

输入验证

// 1. 前端验证
const validator = {
  email: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
  phone: (value) => /^1[3-9]\d{9}$/.test(value),
  url: (value) => {
    try {
      new URL(value)
      return true
    } catch {
      return false
    }
  },
  number: (value) => !isNaN(parseFloat(value)) && isFinite(value),
  integer: (value) => Number.isInteger(Number(value)),
  minLength: (value, min) => value.length >= min,
  maxLength: (value, max) => value.length <= max,
  range: (value, min, max) => value >= min && value <= max
}

// 2. 后端验证(express-validator)
const { body, validationResult } = require('express-validator')

app.post('/api/register',
  body('email').isEmail().normalizeEmail(),
  body('password').isLength({ min: 8 }),
  body('phone').isMobilePhone('zh-CN'),
  body('name').trim().escape(),
  (req, res) => {
    const errors = validationResult(req)
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() })
    }
    // 处理请求
  }
)

// 3. SQL 注入防护
// 错误
const query = `SELECT * FROM users WHERE id = ${userId}`

// 正确:使用参数化查询
const query = 'SELECT * FROM users WHERE id = ?'
db.query(query, [userId])

// 或使用 ORM
const user = await User.findById(userId)

安全最佳实践清单

前端安全

  • 对用户输入进行转义和验证
  • 使用 Content-Security-Policy
  • 设置 HttpOnly、Secure、SameSite Cookie
  • 不在 localStorage 存储敏感数据
  • 使用 HTTPS
  • 移除生产环境的 console.log
  • 依赖包安全审计

后端安全

  • 参数化查询防止 SQL 注入
  • CSRF Token 验证
  • 请求频率限制
  • 密码加密存储
  • 敏感数据脱敏
  • 错误信息不暴露详情
  • 日志不记录敏感信息

部署安全

  • 使用 HTTPS
  • 配置安全响应头
  • 定期更新依赖
  • 启用防火墙
  • 配置备份策略
  • 监控异常访问

下一步

了解 Web 安全后,你可以继续学习:


东巴文(db-w.cn)—— 让 Web 更安全