PHP应用中的CSRF(跨站请求伪造)防御:Token生成、验证与同源策略

PHP 应用中的 CSRF(跨站请求伪造)防御:Token 生成、验证与同源策略

大家好,今天我们来深入探讨 PHP 应用中 CSRF(跨站请求伪造)的防御机制。CSRF 是一种常见的网络攻击,它利用已认证用户的身份,在用户不知情的情况下,冒充用户发起恶意请求。理解 CSRF 的原理,并掌握有效的防御手段,对于保障 Web 应用的安全至关重要。

1. CSRF 攻击原理

CSRF 攻击的核心在于利用用户的身份,在用户已经登录并拥有有效会话的情况下,通过构造恶意链接、表单等方式,诱导用户在不知情的情况下向服务器发送请求。由于请求中包含了用户的身份认证信息(例如 Cookie),服务器会误认为这是用户的合法操作,从而执行恶意操作。

举个例子,假设一个银行网站允许用户通过 POST 请求修改密码:

<form action="https://bank.example.com/change_password" method="POST">
  <input type="password" name="new_password" value="new_password">
  <input type="password" name="confirm_password" value="new_password">
  <button type="submit">修改密码</button>
</form>

攻击者可以构造一个包含恶意密码的表单,并将其嵌入到其他网站或者通过电子邮件发送给用户:

<form action="https://bank.example.com/change_password" method="POST">
  <input type="hidden" name="new_password" value="hacked_password">
  <input type="hidden" name="confirm_password" value="hacked_password">
  <button type="submit">点我领取免费礼品!</button>
</form>
<script>
  document.forms[0].submit(); // 自动提交表单
</script>

如果用户在登录银行网站后点击了这个链接,浏览器会自动提交表单,并将银行网站的 Cookie 一起发送给服务器。服务器会认为这是用户的正常请求,从而修改用户的密码。

2. CSRF 防御的核心思想

CSRF 防御的核心思想是:确认请求的来源是可信的,而不是来自外部的恶意站点。 为了实现这一点,我们需要在请求中加入一些额外的验证信息,让服务器能够区分合法请求和伪造请求。

3. CSRF 防御方法:Token 机制

Token 机制是目前最常用的 CSRF 防御方法。它的原理是:

  1. 在服务器端生成一个随机的、不可预测的 Token。
  2. 将 Token 嵌入到 HTML 表单或者 URL 中。
  3. 当用户提交表单或者点击链接时,Token 会一起发送到服务器。
  4. 服务器验证 Token 的有效性。如果 Token 不正确或者缺失,则拒绝请求。

3.1 Token 生成

Token 必须是随机的、不可预测的,以防止攻击者猜测或者伪造 Token。可以使用 PHP 的 random_bytes() 函数生成随机字节,然后使用 bin2hex() 函数将其转换为十六进制字符串:

<?php
function generateCSRFToken() {
  // 安全地生成随机字节
  $randomBytes = random_bytes(32); // 32 字节足够
  // 将字节转换为十六进制字符串
  $token = bin2hex($randomBytes);
  return $token;
}

// 示例:生成一个 CSRF Token
$csrfToken = generateCSRFToken();
echo "Generated CSRF Token: " . $csrfToken . "n";

?>

为了提高安全性,可以将 Token 与用户的会话绑定。例如,可以将 Token 存储在 $_SESSION 变量中:

<?php
session_start();

function generateCSRFToken() {
  $randomBytes = random_bytes(32);
  $token = bin2hex($randomBytes);
  $_SESSION['csrf_token'] = $token; // 将 Token 存储在会话中
  return $token;
}

function getCSRFToken() {
  if (isset($_SESSION['csrf_token'])) {
    return $_SESSION['csrf_token'];
  } else {
    return generateCSRFToken();
  }
}

// 示例:获取 CSRF Token
$csrfToken = getCSRFToken();
echo "CSRF Token: " . $csrfToken . "n";

?>

3.2 Token 嵌入到 HTML 表单

在 HTML 表单中,可以使用隐藏的 <input> 元素来嵌入 Token:

<form action="process.php" method="POST">
  <input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($csrfToken); ?>">
  <label for="name">Name:</label>
  <input type="text" id="name" name="name"><br><br>
  <input type="submit" value="Submit">
</form>

注意:使用 htmlspecialchars() 函数对 Token 进行转义,以防止 XSS 攻击。

3.3 Token 嵌入到 URL

如果需要通过 GET 请求传递 Token,可以将 Token 作为 URL 参数:

<a href="delete.php?id=123&csrf_token=<?php echo htmlspecialchars($csrfToken); ?>">Delete</a>

同样需要使用 htmlspecialchars() 函数对 Token 进行转义。

3.4 Token 验证

在服务器端,需要验证请求中携带的 Token 是否与会话中存储的 Token 一致:

<?php
session_start();

function validateCSRFToken($token) {
  if (isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token)) {
    // 使用 hash_equals() 函数进行安全比较,防止 timing attack
    unset($_SESSION['csrf_token']); // 验证后删除 Token,防止重复使用
    return true;
  } else {
    return false;
  }
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  if (isset($_POST['csrf_token'])) {
    $csrfToken = $_POST['csrf_token'];
    if (validateCSRFToken($csrfToken)) {
      // Token 验证成功,处理请求
      echo "CSRF Token is valid.n";
      // 处理表单数据
      echo "Name: " . htmlspecialchars($_POST['name']) . "n";
    } else {
      // Token 验证失败,拒绝请求
      http_response_code(403); // 返回 403 Forbidden 状态码
      echo "CSRF Token is invalid.n";
    }
  } else {
    // Token 缺失,拒绝请求
    http_response_code(400); // 返回 400 Bad Request 状态码
    echo "CSRF Token is missing.n";
  }
} else {
  // 显示包含 CSRF Token 的表单
  $csrfToken = $_SESSION['csrf_token'] ?? bin2hex(random_bytes(32));
  $_SESSION['csrf_token'] = $csrfToken;

  echo '<form action="process.php" method="POST">';
  echo '  <input type="hidden" name="csrf_token" value="' . htmlspecialchars($csrfToken) . '">';
  echo '  <label for="name">Name:</label>';
  echo '  <input type="text" id="name" name="name"><br><br>';
  echo '  <input type="submit" value="Submit">';
  echo '</form>';
}
?>
  • hash_equals() 函数: 用于进行安全字符串比较,防止 timing attack。
  • Token 验证后删除: 防止 Token 被重复使用,增加攻击难度。
  • HTTP 状态码: 使用 http_response_code() 函数返回适当的 HTTP 状态码,例如 403 Forbidden (Token 无效) 或 400 Bad Request (Token 缺失)。

4. CSRF 防御方法:SameSite Cookie

SameSite Cookie 是一种浏览器安全机制,可以限制 Cookie 的跨域使用,从而减轻 CSRF 攻击的风险。

SameSite Cookie 有三个可选值:

  • Strict: Cookie 只能在同一站点内使用。如果请求来自不同的站点,浏览器将不会发送 Cookie。
  • Lax: Cookie 在同一站点内以及某些跨站点请求中使用,例如导航到目标 URL 的 GET 请求。
  • None: Cookie 没有 SameSite 限制,可以在任何站点中使用。但是,如果设置 SameSite=None,必须同时设置 Secure 属性,表示 Cookie 只能通过 HTTPS 连接发送。

可以通过 setcookie() 函数设置 SameSite Cookie:

<?php
// 设置一个 SameSite=Strict 的 Cookie
setcookie('session_id', '1234567890', ['samesite' => 'Strict']);

// 设置一个 SameSite=Lax 的 Cookie
setcookie('tracking_id', 'abcdefghij', ['samesite' => 'Lax']);

// 设置一个 SameSite=None 的 Cookie (必须同时设置 Secure 属性)
setcookie('cross_site_cookie', 'klmnopqrst', ['samesite' => 'None', 'secure' => true]);

?>

注意:

  • 并非所有浏览器都支持 SameSite Cookie。
  • 如果需要支持老版本的浏览器,需要结合 Token 机制进行防御。
  • SameSite=None 必须与 Secure 属性一起使用,否则浏览器会拒绝设置 Cookie。

5. CSRF 防御方法:Referer 检查

Referer 头部包含了发起请求的页面的 URL。可以通过检查 Referer 头部来判断请求的来源是否合法。

<?php
function validateReferer($allowedOrigin) {
  $referer = $_SERVER['HTTP_REFERER'] ?? ''; // 获取 Referer 头部,如果不存在则设置为空字符串
  if (empty($referer)) {
    // Referer 头部缺失,可能是直接访问或者某些浏览器策略导致,需要根据实际情况处理
    return false;
  }

  $refererOrigin = parse_url($referer, PHP_URL_SCHEME) . '://' . parse_url($referer, PHP_URL_HOST);

  if ($refererOrigin === $allowedOrigin) {
    return true;
  } else {
    return false;
  }
}

$allowedOrigin = 'https://www.example.com'; // 允许的来源站点

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  if (validateReferer($allowedOrigin)) {
    // Referer 验证成功,处理请求
    echo "Referer is valid.n";
    // 处理表单数据
    echo "Name: " . htmlspecialchars($_POST['name']) . "n";
  } else {
    // Referer 验证失败,拒绝请求
    http_response_code(403); // 返回 403 Forbidden 状态码
    echo "Referer is invalid.n";
  }
} else {
  // 显示包含 Referer 检查的表单
  echo '<form action="process.php" method="POST">';
  echo '  <label for="name">Name:</label>';
  echo '  <input type="text" id="name" name="name"><br><br>';
  echo '  <input type="submit" value="Submit">';
  echo '</form>';
}
?>

注意:

  • Referer 头部可以被伪造,因此 Referer 检查不是一种完全可靠的 CSRF 防御方法。
  • 某些浏览器或者安全软件可能会禁用 Referer 头部。
  • Referer 检查可以作为一种辅助的防御手段,与其他防御方法结合使用。

6. CSRF 防御方法比较

防御方法 优点 缺点 适用场景
Token 机制 安全性高,可靠性强 需要在服务器端生成和验证 Token,增加开发复杂度 所有需要保护的敏感操作,例如修改密码、转账等
SameSite Cookie 配置简单,可以减轻 CSRF 攻击的风险 并非所有浏览器都支持,需要结合 Token 机制进行防御,SameSite=None需要HTTPS支持 减轻 CSRF 攻击风险,可以作为辅助防御手段
Referer 检查 配置简单,可以快速判断请求的来源 Referer 头部可以被伪造,不是完全可靠的防御方法,某些浏览器会禁用 Referer,只能作为辅助防御手段 可以作为辅助防御手段,与其他防御方法结合使用

7. 同源策略(Same-Origin Policy)

同源策略是浏览器的一种安全机制,用于限制来自不同源的文档或脚本之间的交互。同源是指协议、域名和端口号都相同。

同源策略主要限制以下行为:

  • 跨域读取 Cookie、LocalStorage 和 IndexDB。
  • 跨域访问 DOM。
  • 跨域发起 XMLHttpRequest 请求。

同源策略可以有效防止恶意网站窃取用户的敏感信息。

8. 如何绕过同源策略?

虽然同源策略限制了跨域访问,但在某些情况下,我们需要进行跨域操作。以下是一些常用的绕过同源策略的方法:

  • JSONP: 利用 <script> 标签的跨域特性,通过动态创建 <script> 标签来发送跨域请求。JSONP 只能支持 GET 请求。
  • CORS(跨域资源共享): 服务器端设置 Access-Control-Allow-Origin 头部,允许指定的域名进行跨域访问。
  • 代理服务器: 在同源服务器上设置代理,将跨域请求转发到目标服务器。

注意: 绕过同源策略需要谨慎操作,确保目标服务器是可信的,以防止安全风险。

9. 总结需要注意的点

CSRF 防御是一个复杂的问题,需要综合考虑各种因素。以下是一些建议:

  • 选择合适的防御方法: 根据应用的实际情况选择合适的防御方法。通常情况下,Token 机制是首选的防御方法。
  • 结合多种防御方法: 结合 Token 机制、SameSite Cookie 和 Referer 检查等多种防御方法,提高安全性。
  • 定期更新和维护: 定期更新和维护安全策略,及时修复漏洞。
  • 加强用户教育: 教育用户提高安全意识,避免点击不明链接和下载可疑文件。
  • 安全编码: 使用 htmlspecialchars() 函数对输出进行转义,防止 XSS 攻击。使用 hash_equals() 函数进行安全字符串比较,防止 timing attack。

理解并应用这些措施,保护你的应用免受攻击

理解 CSRF 攻击的原理,并掌握有效的防御手段,对于保障 Web 应用的安全至关重要。希望今天的讲座能够帮助大家更好地理解 CSRF 防御机制,并在实际开发中应用这些知识,构建更加安全的 Web 应用。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注