PHP中的CSRF保护
跨站请求伪造被简写为CSRF。它是一种黑客攻击行为,黑客会逼迫你对你目前已经登录的网站做一些事情。反CSRF的实施减少了网站的脆弱性。
有了这种保护,网站就会拒绝那些发送没有CSRF标记或错误CSRF标记的请求的恶意访问。下图显示了针对CSRF攻击的用户请求验证。
如果一个真正的用户用适当的令牌发布表单,服务器会处理该请求。否则,在没有CSRF令牌参数的情况下,它将拒绝。
本教程将展示一个带有CSRF保护的PHP联系表单的例子。有了这种保护,它将在处理请求之前确保其真实性。
此外,我们将在PHP中创建一个服务来处理针对CSRF攻击的安全验证。服务器将拒绝没有令牌或无效令牌的用户的请求。
在本指南结束时,读者将了解以下内容。
要完成本教程,你将需要。
这段代码可以保护一个PHP联系表单免受CSRF攻击。首先,它创建了一个联系表单。然后,这个表单的后处理程序检查用户请求的CSRF攻击。最后,当登陆页面被加载时,PHP脚本会生成CSRF令牌。
这个令牌将是表单页脚中的一个隐藏字段。它也会在PHP会话中照顾到这个令牌。当表单提交时,PHP代码将检查CSRF令牌参数。如果会话中的令牌被发现,它将被验证。
如果用户发送的请求没有包括CSRF令牌,服务器将拒绝该请求。如果令牌与会话中的令牌不匹配,服务器也将拒绝该请求。
如果CSRF令牌被成功验证,服务器将向目标地址发送联系邮件。下图显示了这个例子的文件结构。
让我们开始吧!
第1步:创建一个PHP会话并生成一个CSRF令牌
着陆页上的表单页脚脚本调用
SecurityService
。这个类在PHP中生成一个CSRF令牌。它将令牌保存在一个PHP会话中,以便以后使用。它将帮助处理表单提交后的CSRF验证。
表单页脚是一个框架文件,用生成的令牌加载一个隐藏字段。例如,下面的代码摘录来自
SecurityService.php
文件,生成了一个CSRF令牌。
本文的下一节将介绍服务类的完整代码。
第2步:用CSRF令牌渲染联系表格
这是一个带有姓名、电子邮件、主题和信息等常规字段的HTML联系表。此外,还有一个隐藏字段csrf-token,里面有生成的令牌。
提交动作在将参数发布到PHP之前处理jQuery表单验证。
客户端验证脚本处理提交时的基本验证。它对每个字段进行非空检查。
<!DOCTYPE html>
<html lang="en">
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- title -->
<title>PHP CSRF PROTECTION</title>
<!-- Default stylesheet -->
<link rel="stylesheet" href="css/style.css">
<!-- JQuery -->
<script src="vendor/jquery/jquery-3.2.1.min.js"></script>
<!-- styles -->
<style>
.error-field {
border: 1px solid #d96557;
.send-button {
cursor: pointer;
background: #3cb73c;
border: #36a536 1px solid;
color: #FFF;
font-size: 1em;
width: 100px;
</style>
</head>
<!-- main container -->
<div class="container">
<!-- header -->
<h3>PHP CSRF PROTECTION</h3>
<!-- POST form -->
<form action="" method="post" id="frm-contact" onsubmit="return validateContactForm()">
<!-- row userName-->
<div class="row">
<!-- userName label -->
<div class="label">
Name: <span id="userName-info" class="validation-message"></span>
</div>
<input type="text" name="userName" id="userName" class="frm-input" value="<?php if (!empty($_POST['userName'])&& $type == 'error') {
echo $_POST['userName'];
}?>">
<!-- row userName ends here-->
<!-- row email-->
<div class="row">
<!-- email label -->
<div class="label">
Email: <span id="email" class="validation-message"></span>
<input type="email" name="email" id="email" class="frm-input" value="<?php if (!empty($_POST['email'])&& $type == 'error') {
echo $_POST['email'];
}?>">
<!-- row email ends here-->
<!-- row userName-->
<div class="row">
<!-- subject label -->
<div class="label">
Subject: <span id="subject-info" class="validation-message"></span>
<input type="text" name="subject" id="subject" class="frm-input" value="<?php if (!empty($_POST['subject'])&& $type == 'error') {
echo $_POST['userName'];
}?>">
<!-- row subject ends here-->
<!-- row message starts here-->
<div class="row">
<div class="label">
Message: <span id="userMessage-info" class="validation-message"></span>
<textarea name="content" id="content" class="phppot-input" cols="60" rows="6"></textarea>
<!-- row message ends here-->
<!-- submit/send info -->
<div class="row">
<input type="submit" name="send" class="send-button" value="Send" />
</form>
<script src="assets/js/validate.js"></script>
</body>
</html>
表单页脚脚本触发服务处理程序来生成令牌。insertHiddenToken()
编写HTML代码,将csrf令牌字段加载到表单中。
view/framework/form-footer.php
require_once __DIR__ . '/../../lib/SecurityService.php';
$antiCSRF = new SecurityService\securityService();
$antiCSRF->insertHiddenToken();
第3步:在PHP中进行反跨站请求伪造(CSRF)验证
提交嵌入令牌的联系表单时,表单动作会执行以下脚本。SecurityService的validate()
函数将提交的令牌与存储在会话中的令牌进行比较。
如果发现匹配,它将进一步发送联系电子邮件。否则,它将向用户提供一个错误信息。
index.php (PHP CSRF验证和表单处理)
use MailService;
session_start();
if (!empty($_POST['send'])) {
require_once __DIR__ . '/lib/SecurityService.php';
$antiCSRF = new SecurityService\securityService();
$csrfResponse = $antiCSRF->validate();
if (!empty($csrfResponse)) {
require_once __DIR__ . '/lib/MailService.php';
$mailService = new MailService();
$response = $mailService->sendContactMail($_POST);
if (!empty($response)) {
$message = "Hi, we have received your message. Thank you.";
$type = "success";
} else {
$message = "Unable to send email.";
$type = "error";
} else {
$message = "Security alert: Unable to process your request.";
$type = "error";
第4步:生成、插入、验证CSRF令牌的安全服务
这个在PHP中创建的服务类包括处理CSRF保护相关操作的方法。它定义了一个类属性来设置表单令牌字段名,会话索引。此外,它有方法生成令牌并将其写入HTML和PHP会话中。
此外,它使用XSS缓解,同时用令牌写入表单页脚。此外,它还可以从验证过程中排除一些URLs。
被排除的URL会绕过CSRF验证过程。相反,代码从PHP SERVER变量中获取当前请求的URL。
然后将其与排除的URL数组进行比较,跳过验证。
<? php
namespace SecurityService;
class securityService
private $formTokenLabel = 'eg-csrf-token-label';
private $sessionTokenLabel = 'EG_CSRF_TOKEN_SESS_IDX';
private $post = [];
private $session = [];
private $server = [];
private $excludeUrl = [];
private $hashAlgo = 'sha256';
private $hmac_ip = true;
private $hmacData = 'ABCeNBHVe3kmAqvU2s7yyuJSF2gpxKLC';
public function __construct($excludeUrl = null, &$post = null, &$session = null, &$server = null)
if (! \is_null($excludeUrl)) {
$this->excludeUrl = $excludeUrl;
if (! \is_null($post)) {
$this->post = & $post;
} else {
$this->post = & $_POST;
if (! \is_null($server)) {
$this->server = & $server;
} else {
$this->server = & $_SERVER;
if (! \is_null($session)) {
$this->session = & $session;
} elseif (! \is_null($_SESSION) && isset($_SESSION)) {
$this->session = & $_SESSION;
} else {
throw new \Error('No session available for persistence');
public function insertHiddenToken()
$csrfToken = $this->getCSRFToken();
echo "<!--\n--><input type=\"hidden\"" . " name=\"" . $this->xssafe($this->formTokenLabel) . "\"" . " value=\"" . $this->xssafe($csrfToken) . "\"" . " />";
public function xssafe($data, $encoding = 'UTF-8')
return htmlspecialchars($data, ENT_QUOTES | ENT_HTML401, $encoding);
public function getCSRFToken()
if (empty($this->session[$this->sessionTokenLabel])) {
$this->session[$this->sessionTokenLabel] = bin2hex(openssl_random_pseudo_bytes(32));
if ($this->hmac_ip !== false) {
$token = $this->hMacWithIp($this->session[$this->sessionTokenLabel]);
} else {
$token = $this->session[$this->sessionTokenLabel];
return $token;
private function hMacWithIp($token)
$hashHmac = \hash_hmac($this->hashAlgo, $this->hmacData, $token);
return $hashHmac;
private function getCurrentRequestUrl()
$protocol = "http";
if (isset($this->server['HTTPS'])) {
$protocol = "https";
$currentUrl = $protocol . "://" . $this->server['HTTP_HOST'] . $this->server['REQUEST_URI'];
return $currentUrl;
public function validate()
$currentUrl = $this->getCurrentRequestUrl();
if (! in_array($currentUrl, $this->excludeUrl)) {
if (! empty($this->post)) {
$isAntiCSRF = $this->validateRequest();
if (! $isAntiCSRF) {
// CSRF attack attempt
// CSRF attempt is detected. Need not reveal that information
// to the attacker, so just failing without info.
// Error code 1837 stands for CSRF attempt and this is for
// our identification purposes.
return false;
return true;
public function isValidRequest()
$isValid = false;
$currentUrl = $this->getCurrentRequestUrl();
if (! in_array($currentUrl, $this->excludeUrl)) {
if (! empty($this->post)) {
$isValid = $this->validateRequest();
return $isValid;
public function validateRequest()
if (!isset($this->session[$this->sessionTokenLabel])) {
// CSRF Token not found
return false;
if (!empty($this->post[$this->formTokenLabel])) {
// Let's pull the POST data
$token = $this->post[$this->formTokenLabel];
} else {
return false;
if (is_string($token)) {
return false;
// Grab the stored token
if ($this->hmac_ip !== false) {
$expected = $this->hMacWithIp($this->session[$this->sessionTokenLabel]);
} else {
$expected = $this->session[$this->sessionTokenLabel];
return \hash_equals($token, $expected);
* removes the token from the session
public function unsetToken()
if (! empty($this->session[$this->sessionTokenLabel])) {
unset($this->session[$this->sessionTokenLabel]);
这个MailService.php
使用PHP核心mail()
函数来发送联系邮件。你可以用SMTP通过电子邮件发送脚本来代替它。检查这个以使用PHP获得一个IP地址。它可能对记录用户的IP地址很有用。
MailService.php
namespace csrfProtection;
class MailService
function sendContactMail($postValues)
$name = $postValues["userName"];
$email = $postValues["userEmail"];
$subject = $postValues["subject"];
$content = $postValues["content"];
$toEmail = "ADMIN EMAIL";
$mailHeaders = "From: " . $name . "(" . $email . ")\r\n";
$response = mail($toEmail, $subject, $content, $mailHeaders);
return $response;
输出:来自服务器的CSRF验证响应
下面的图片显示了通常的联系表单。我们之前在很多联系表单教程中都看到过这个输出。在表单界面下面,图片显示了红色的安全警报信息。它确认了那些使用错误或空令牌发送请求的用户。
这样我们就在PHP联系表单中实现了反CSRF保护。我希望这个例子的代码是有用的,你能得到我们在这里讨论的实施过程。
我们已经在PHP中创建了一个SecurityService类来处理CSRF保护。在你需要启用CSRF保护的地方,它可以在多个应用程序中重复使用。返回响应信息的PHP代码正确地确认了用户。