网络安全实战实践笔记

这次做网络安全加固,从 XSS 到 CSRF,。

XSS 攻击

反射型 XSS

<!-- 恶意 URL -->
http://example.com/search?q=<script>alert('XSS')</script>

<!-- 页面渲染 -->
<div>搜索结果: <script>alert('XSS')</script></div>

存储型 XSS

// 用户提交包含恶意脚本的内容
const comment = '<script>alert("XSS")</script>';
db.insert('comments', { comment });

// 页面渲染时直接输出
<div>${comment}</div>

DOM 型 XSS

// 从 URL 获取参数
const param = new URLSearchParams(window.location.search).get('data');

// 直接操作 DOM
document.getElementById('output').innerHTML = param;

XSS 防御

输出编码

// 使用模板引擎自动编码
// EJS
<div><%= userComment %></div>

// React(自动转义)
<div>{userComment}</div>

// Vue
<div>{{ userComment }}</div>

输入验证

// 白名单验证
function sanitizeInput(input) {
  // 只允许字母、数字、空格
  return input.replace(/[^a-zA-Z0-9\s]/g, '');
}

// 使用验证库
const validator = require('validator');

if (!validator.isEmail(userEmail)) {
  throw new Error('Invalid email');
}

Content Security Policy

# CSP 头
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'; font-src 'self'; frame-src 'none'; object-src 'none'; base-uri 'self';
# 设置 HTTPOnly Cookie
Set-Cookie: sessionid=abc123; HttpOnly; Secure; SameSite=Strict

CSRF 攻击

攻击原理

<!-- 恶意网站 -->
<img src="https://bank.example.com/transfer?to=hacker&amount=10000">

CSRF Token

// 生成 CSRF Token
const crypto = require('crypto');

function generateCSRFToken() {
  return crypto.randomBytes(32).toString('hex');
}

// 在 Session 中存储 Token
app.use((req, res, next) => {
  if (!req.session.csrfToken) {
    req.session.csrfToken = generateCSRFToken();
  }
  next();
});

// 在表单中包含 Token
app.get('/form', (req, res) => {
  res.send(`
    <form method="POST" action="/submit">
      <input type="hidden" name="csrf_token" value="${req.session.csrfToken}">
      <input type="text" name="data">
      <button type="submit">Submit</button>
    </form>
  `);
});

// 验证 Token
app.post('/submit', (req, res) => {
  const token = req.body.csrf_token;
  
  if (token !== req.session.csrfToken) {
    return res.status(403).send('Invalid CSRF token');
  }
  
  // 处理表单提交
  ...
});
# 设置 SameSite Cookie
Set-Cookie: sessionid=abc123; SameSite=Strict

SQL 注入

注入示例

// 不安全的查询(易受 SQL 注入)
const query = `SELECT * FROM users WHERE username = '${username}' AND password = '${password}'`;
db.query(query);

// 恶意输入
username = "admin' --"
password = "anything"

// 实际执行的 SQL
SELECT * FROM users WHERE username = 'admin' --' AND password = 'anything'

防御方法

// 使用参数化查询
const query = 'SELECT * FROM users WHERE username = ? AND password = ?';
db.query(query, [username, password]);

// 使用 ORM
const user = await User.findOne({
  where: {
    username,
    password
  }
});

// 使用输入验证
if (!validator.isAlphanumeric(username)) {
  throw new Error('Invalid username');
}

认证与授权

密码存储

const bcrypt = require('bcrypt');

// 哈希密码
const saltRounds = 10;
const hashedPassword = await bcrypt.hash(password, saltRounds);

// 验证密码
const isValid = await bcrypt.compare(password, hashedPassword);

JWT 认证

const jwt = require('jsonwebtoken');

// 生成 Token
const token = jwt.sign(
  { userId: user.id },
  process.env.JWT_SECRET,
  { expiresIn: '1h' }
);

// 验证 Token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
console.log(decoded.userId);

基于角色的访问控制(RBAC)

// 用户模型
const User = {
  roles: ['user', 'admin']
};

// 权限检查
function hasPermission(user, permission) {
  const rolePermissions = {
    user: ['read'],
    admin: ['read', 'write', 'delete']
  };
  
  return user.roles.some(role => 
    rolePermissions[role]?.includes(permission)
  );
}

// 中间件
function requirePermission(permission) {
  return (req, res, next) => {
    if (!hasPermission(req.user, permission)) {
      return res.status(403).send('Permission denied');
    }
    next();
  };
}

// 使用中间件
app.get('/admin', requirePermission('write'), (req, res) => {
  res.send('Admin page');
});

HTTPS 加密

配置 HTTPS

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate /etc/ssl/certs/example.com.crt;
    ssl_certificate_key /etc/ssl/private/example.com.key;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    ssl_prefer_server_ciphers on;

    # HSTS
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}

安全头

# 安全响应头
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=()

安全监控

安全日志

// 记录安全事件
function logSecurityEvent(event) {
  logger.info({
    type: 'security',
    event: event.type,
    userId: event.userId,
    ip: event.ip,
    userAgent: event.userAgent,
    timestamp: new Date()
  });
}

// 登录失败
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  
  const user = await User.findOne({ username });
  if (!user || !await bcrypt.compare(password, user.password)) {
    logSecurityEvent({
      type: 'login_failed',
      userId: user?.id,
      ip: req.ip,
      userAgent: req.headers['user-agent']
    });
    
    return res.status(401).send('Invalid credentials');
  }
  
  logSecurityEvent({
    type: 'login_success',
    userId: user.id,
    ip: req.ip,
    userAgent: req.headers['user-agent']
  });
  
  const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET);
  res.json({ token });
});

安全告警

// 检测异常行为
async function detectSuspiciousActivity(userId) {
  const recentAttempts = await db.query(`
    SELECT COUNT(*) as count
    FROM security_events
    WHERE user_id = ? AND type = 'login_failed' 
    AND timestamp > DATE_SUB(NOW(), INTERVAL 1 HOUR)
  `, [userId]);
  
  if (recentAttempts[0].count > 5) {
    // 发送告警
    sendAlert({
      type: 'suspicious_activity',
      userId,
      attempts: recentAttempts[0].count
    });
    
    // 锁定账户
    await User.update(userId, { locked: true });
  }
}

踩过的坑

坑一:忘记转义输出

使用了模板引擎,但忘记转义输出。

解决:始终使用模板引擎的转义功能。

坑二:Cookie 没有设置 HttpOnly

Cookie 可以被 JavaScript 访问,导致 XSS 攻击。

解决:设置 HttpOnly 和 Secure。

res.cookie('sessionid', sessionId, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict'
});

坑三:敏感信息泄露

错误信息中包含敏感信息。

解决:生产环境不暴露详细信息。

if (process.env.NODE_ENV === 'production') {
  res.status(500).send('Internal server error');
} else {
  res.status(500).send(error.message);
}

安全检查清单

输入验证

  • 所有输入都经过验证
  • 使用白名单验证
  • 限制输入长度

输出编码

  • HTML 内容正确转义
  • URL 参数正确编码
  • JSON 正确转义

认证授权

  • 密码正确哈希
  • Token 正确验证
  • 权限正确检查

数据保护

  • HTTPS 启用
  • Cookie 安全设置
  • 敏感数据加密

监控告警

  • 安全日志记录
  • 异常行为检测
  • 安全事件告警

写在最后

网络安全这东西,不是技术问题,是意识和流程问题。

解决了

  • XSS 攻击
  • CSRF 攻击
  • SQL 注入
  • 认证授权问题

带来了

  • 开发成本
  • 性能影响
  • 用户体验变化

安全加固之前先评估:

  • 业务需求
  • 用户规模
  • 预算
  • 法规要求

不是所有安全措施都需要,但基础安全不能少。


这次网络安全加固花了一个月,从 XSS 到 CSRF,再到完整的安全体系。加固完成后,安全漏洞数量从 10 个降到 0 个,安全事件减少了 90%。

可用性说明:本文发布于 2019 年 6 月,距今已超过五年。文中涉及的软件版本、接口、下载地址、命令参数和操作界面可能已经发生变化,部分方案在当前环境下可能失效。请结合官方最新文档核对后再操作,生产环境使用前务必先行验证。

版权声明: 本文首发于 指尖魔法屋-网络安全实战实践笔记https://blog.thinkmoon.cn/post/76-network-security-xss-csrf-defense-guide/) 转载或引用必须申明原指尖魔法屋来源及源地址!