Skip to content

表单安全

表单是用户与网站交互的主要入口,也是安全攻击的重点目标。CSRF(跨站请求伪造)、输入验证不足、CAPTCHA 绕过等问题可能导致数据泄露和恶意操作。本节将介绍 CSRF 防护策略、客户端与服务端输入验证、CAPTCHA 实现方法。

前置知识

阅读本节前,建议先了解:iframe 安全

CSRF 防护

什么是 CSRF

CSRF(Cross-Site Request Forgery)利用用户已认证的会话,在用户不知情的情况下发送恶意请求。

用户已登录银行网站 → 访问恶意网站 → 恶意网站发送银行转账请求 → 银行执行转账

CSRF Token

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>CSRF 防护示例</title>
</head>
<body>
  <form action="/transfer" method="POST">
    <!-- 服务器生成的 CSRF Token -->
    <input type="hidden" name="csrf_token" value="a1b2c3d4e5f6" />

    <label for="to-account">转账到</label>
    <input type="text" id="to-account" name="to_account" />

    <label for="amount">金额</label>
    <input type="number" id="amount" name="amount" min="0.01" step="0.01" />

    <button type="submit">确认转账</button>
  </form>
</body>
</html>
# 服务器设置 SameSite Cookie
Set-Cookie: session=abc123; SameSite=Strict; Secure; HttpOnly

# SameSite 值
# Strict: 完全禁止跨站 Cookie(最安全)
# Lax: 允许安全的跨站 GET 请求
# None: 允许所有跨站请求(不安全)
html
<!-- 服务器在 Cookie 和表单中都设置 Token -->
<form action="/transfer" method="POST">
  <!-- 从 Cookie 中读取的 Token -->
  <input type="hidden" name="csrf_token" value="..." />
  <!-- 服务器对比 Cookie 和表单中的 Token 是否一致 -->
  <button type="submit">提交</button>
</form>

输入验证

客户端验证

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>表单验证示例</title>
</head>
<body>
  <form id="register-form" action="/register" method="POST">
    <div>
      <label for="username">用户名</label>
      <input
        type="text"
        id="username"
        name="username"
        required
        minlength="3"
        maxlength="20"
        pattern="[a-zA-Z0-9_]+"
        aria-describedby="username-hint"
      />
      <span id="username-hint">3-20 个字符,仅支持字母、数字和下划线</span>
    </div>

    <div>
      <label for="email">邮箱</label>
      <input
        type="email"
        id="email"
        name="email"
        required
        aria-describedby="email-hint"
      />
      <span id="email-hint">请输入有效的邮箱地址</span>
    </div>

    <div>
      <label for="age">年龄</label>
      <input
        type="number"
        id="age"
        name="age"
        min="18"
        max="120"
        required
      />
    </div>

    <div>
      <label for="website">个人网站(可选)</label>
      <input type="url" id="website" name="website" />
    </div>

    <button type="submit">注册</button>
  </form>

  <script>
    // 客户端验证(JavaScript)
    const form = document.getElementById('register-form');

    form.addEventListener('submit', function(e) {
      // 自定义验证
      const username = document.getElementById('username').value;
      if (username.includes('admin') || username.includes('root')) {
        e.preventDefault();
        alert('用户名不能包含保留词');
        return;
      }

      // 如果有自定义验证,使用 setCustomValidity
      const email = document.getElementById('email');
      if (email.value.endsWith('.test')) {
        email.setCustomValidity('不支持 .test 域名的邮箱');
        e.preventDefault();
      } else {
        email.setCustomValidity('');
      }
    });
  </script>
</body>
</html>

服务端验证(必须)

# 服务端必须验证所有输入
# 1. 类型验证:确保输入是预期的类型
# 2. 长度验证:限制输入长度
# 3. 格式验证:正则匹配
# 4. 范围验证:确保值在允许范围内
# 5. 白名单验证:仅允许已知的合法值

HTML5 内置验证属性

属性说明
required必填字段
minlength / maxlength文本长度限制
min / max数值范围限制
pattern正则表达式验证
type="email"邮箱格式验证
type="url"URL 格式验证
type="number"仅允许数字

CAPTCHA

html
<!-- reCAPTCHA v2 -->
<div class="g-recaptcha" data-sitekey="your-site-key"></div>
<script src="https://www.google.com/recaptcha/api.js" async defer></script>

<!-- reCAPTCHA v3(无用户交互) -->
<script src="https://www.google.com/recaptcha/api.js?render=your-site-key"></script>
<script>
  grecaptcha.ready(function() {
    grecaptcha.execute('your-site-key', { action: 'submit' }).then(function(token) {
      // 将 token 添加到表单
      document.getElementById('recaptcha-token').value = token;
    });
  });
</script>
<input type="hidden" id="recaptcha-token" name="recaptcha_token" />

<!-- Turnstile(Cloudflare 替代方案) -->
<div class="cf-turnstile" data-sitekey="your-site-key" data-callback="javascriptCallback"></div>

HTTP 方法安全

html
<!-- 敏感操作使用 POST 而非 GET -->
<!-- 错误:GET 请求可能导致 CSRF 攻击更容易 -->
<a href="/delete-account">删除账户</a>

<!-- 正确:使用 POST + CSRF Token -->
<form action="/delete-account" method="POST">
  <input type="hidden" name="csrf_token" value="..." />
  <button type="submit" onclick="return confirm('确定要删除账户吗?')">删除账户</button>
</form>

注意事项

  1. 永远不要只依赖客户端验证:服务端必须验证所有输入
  2. CSRF Token 每次请求都应更新:防止 Token 重放
  3. 敏感操作需要二次确认:转账、删除等操作需要确认
  4. 限制请求频率:防止暴力攻击
  5. 记录所有敏感操作:安全审计需要日志

最佳实践

  • 所有表单添加 CSRF Token
  • 设置 SameSite=Strict Cookie
  • 客户端 + 服务端双重验证
  • 使用 HTML5 内置验证属性
  • 敏感操作使用 POST 方法
  • 重要操作添加 CAPTCHA
  • 限制请求频率防止暴力攻击
  • 记录安全审计日志

下一节

继续学习:子资源完整性(SRI)

参考链接