PHP中批量删除条目时CSRF令牌验证失败的原因及解决
PHP CSRF令牌验证问题:批量/连续删除失败的原因与修复方案
问题现象
当尝试使用同一CSRF令牌删除多个条目,或同一会话内连续发起删除请求时,会触发“CSRF attack detected”错误,验证失败。
失败原因分析
从提供的代码来看,核心问题在于令牌更新时机与前端同步不匹配:
- 每次处理
delete_entry请求时,无论验证成功还是失败,都会立即重新生成$_SESSION['auth_token']:- 验证失败分支:执行令牌更新后直接exit
- 验证成功分支:删除操作完成后同样更新令牌
- 前端表单中的
auth_token是页面加载时生成的静态值,请求完成后页面未刷新,表单仍持有旧令牌。下一次提交时,session中的令牌已经更新,导致前后令牌不匹配,触发验证失败。 - 当前表单仅支持单个
target_id提交,无法直接处理批量删除请求,但这不是令牌验证失败的直接原因。
修复方案
根据需求场景,提供两种可行的修复思路:
思路1:允许令牌在同一会话内多次复用(简化方案)
如果不需要严格的一次性令牌,可以移除每次请求后更新令牌的逻辑,仅在会话初始化时生成令牌,直到会话结束或用户主动登出才更新。这种方式安全性略低,但能满足连续删除的需求,适合低风险场景:
// 移除以下两处代码: // 1. 验证失败分支中的令牌更新 // $_SESSION['auth_token'] = bin2hex(random_bytes(20)); // 2. 验证成功分支末尾的令牌更新 // $_SESSION['auth_token'] = bin2hex(random_bytes(20));
思路2:保持一次性令牌机制,同步更新前端令牌(安全方案)
如果需要严格的一次性令牌(更安全),则需要在每次请求完成后,更新前端表单中的令牌值,确保下一次提交的令牌与session一致。推荐用AJAX处理删除请求,步骤如下:
1. 修改后端代码,返回新令牌
在删除操作完成后,返回新生成的令牌(替代直接输出HTML/JS):
// 替换验证成功分支末尾的代码: // $_SESSION['auth_token'] = bin2hex(random_bytes(20)); // echo '<script>$(document).ready(function(){toastr.success("Entry successfully deleted");}) </script>'; // 改为: $new_token = bin2hex(random_bytes(20)); $_SESSION['auth_token'] = $new_token; echo json_encode([ 'status' => 'success', 'message' => 'Entry successfully deleted', 'new_token' => $new_token ]); exit(); // 验证失败分支也返回新令牌: $new_token = bin2hex(random_bytes(20)); $_SESSION['auth_token'] = $new_token; echo json_encode([ 'status' => 'error', 'message' => 'CSRF attack detected. Please try again.', 'new_token' => $new_token ]); exit();
2. 修改前端表单,用AJAX提交请求
<!-- 修改表单,移除submit按钮的默认提交行为 --> <form id="deleteForm" method="POST" action=""> <div class="mb-3"> <input type="hidden" name="auth_token" id="auth_token" value="<?php echo htmlspecialchars(strip_tags($antiXss->xss_clean($_SESSION['auth_token']))); ?>" required> <input type="hidden" name="target_id" class="form-control" id="recipient-name"> </div> </form> <!-- 修改删除按钮的事件 --> <button type="button" id="deleteBtn" class="btn btn-danger"><i class="fa-solid fa-trash"></i> delete </button> <script> $(document).ready(function() { $('#deleteBtn').click(function() { var formData = $('#deleteForm').serialize(); $.ajax({ type: 'POST', url: '', data: formData, dataType: 'json', success: function(response) { // 更新表单中的令牌 $('#auth_token').val(response.new_token); // 显示提示 if (response.status === 'success') { toastr.success(response.message); $('#staticBackdrop').modal('hide'); } else { toastr.error(response.message); } }, error: function() { toastr.error('Request failed'); } }); }); }); </script>
3. 批量删除的适配
如果需要批量删除,可以修改表单,用复选框收集多个target_id,提交时一次性传给后端,后端循环处理,只需验证一次令牌即可:
<!-- 批量删除表单示例 --> <form id="batchDeleteForm" method="POST" action=""> <input type="hidden" name="auth_token" id="batch_auth_token" value="<?php echo htmlspecialchars($_SESSION['auth_token']); ?>" required> <!-- 用复选框选择要删除的条目 --> <div class="mb-3"> <?php foreach($entries as $entry): ?> <input type="checkbox" name="target_ids[]" value="<?php echo $entry['jnum']; ?>"> <?php echo $entry['jnum']; ?> <?php endforeach; ?> </div> <button type="button" id="batchDeleteBtn" class="btn btn-danger">批量删除</button> </form>
后端处理批量删除:
if (isset($_POST["batch_delete_entry"])) { // 验证令牌(仅验证一次) if (!isset($_POST['auth_token']) || $_SESSION['auth_token'] !== $_POST['auth_token'] || empty($_POST['target_ids'])) { $new_token = bin2hex(random_bytes(20)); $_SESSION['auth_token'] = $new_token; echo json_encode([ 'status' => 'error', 'message' => 'Invalid request', 'new_token' => $new_token ]); exit(); } else { // 循环处理每个target_id foreach($_POST['target_ids'] as $target_id) { // 执行删除逻辑(和之前的单个删除逻辑一致) } // 生成新令牌并返回 $new_token = bin2hex(random_bytes(20)); $_SESSION['auth_token'] = $new_token; echo json_encode([ 'status' => 'success', 'message' => 'Entries deleted successfully', 'new_token' => $new_token ]); exit(); } }
内容的提问来源于stack exchange,提问作者dezeer
相关产品推荐
相关产品推荐

