PHP 8环境下CSRF令牌与Session令牌对比失败问题排查
生产环境PHP 8.1.15下CSRF令牌与Session令牌不匹配问题排查
问题现象
- 本地测试服务器CSRF令牌校验正常,部署到远程生产服务器(Apache 2.4 + PHP-FPM 8.1.15)后,每次保存/刷新操作,POST提交的CSRF令牌与Session中存储的令牌始终不一致。
- 仅移除令牌对比校验逻辑后,配置编辑功能可正常工作。
- 切换PHP7到PHP8.1.15测试,问题仅出现在生产环境的PHP8中,且已确认Session Cookie域名配置正确。
已尝试的Session初始化配置
ini_set('session.use_only_cookies', '1'); // 避免PHP通过URL传递Session ini_set('session.auto_start', '1'); // 防止php.ini中已开启session_start()时报错 ini_set('session.use_trans_sid', '0'); ini_set('session.cookie_samesite', 'Strict'); ini_set('session.cookie_httponly', 'true'); ini_set('session.cookie_secure', $secureCookie); ini_set('session.cookie_domain', $_SERVER['SERVER_NAME']); ini_set('url_rewriter.tags', '');
相关代码片段
令牌校验逻辑(已注释)
if ($user->perm->hasPermission($user->getUserId(), 'editconfig')) { // 通过URL参数定义操作:user_action= $userAction = Filter::filterInput(INPUT_GET, 'config_action', FILTER_UNSAFE_RAW, 'listConfig'); $csrfToken = Filter::filterInput(INPUT_POST, 'csrf', FILTER_UNSAFE_RAW); $currentToken = $user->getCsrfTokenFromSession(); echo 'csrfToken : '; var_dump($csrfToken); echo '<br />'; echo 'currentToken : '; var_dump($currentToken); echo '<br />'; echo 'Session : '; var_dump($_SESSION); // 保存配置 //if ('saveConfig' === $userAction && $currentToken === $csrfToken) { # 此处对比当前令牌与CSRF令牌存在问题? # 只有注释掉上面一行,配置编辑器才能正常工作! if ('saveConfig' === $userAction) { $checks = [ 'filter' => FILTER_UNSAFE_RAW, 'flags' => FILTER_REQUIRE_ARRAY, ]; $editData = Filter::filterInputArray(INPUT_POST, ['edit' => $checks]); $userAction = 'listConfig'; $oldConfigValues = $faqConfig->config; // 设置新值 $forbiddenValues = ['{', '}', '$']; $newConfigValues = []; $escapeValues = [ 'main.contactInformations', 'main.customPdfHeader', 'main.customPdfFooter', 'main.titleFAQ', 'main.metaKeywords' ]; // 特殊检查 if (isset($editData['edit']['main.enableMarkdownEditor'])) { $editData['edit']['main.enableWysiwygEditor'] = false; // 启用Markdown编辑器时禁用WYSIWG编辑器 } foreach ($editData['edit'] as $key => $value) { // 移除禁用字符 $newConfigValues[$key] = str_replace($forbiddenValues, '', $value); // 转义部分值 if (isset($escapeValues[$key])) { $newConfigValues[$key] = Strings::htmlspecialchars($value, ENT_QUOTES); } $keyArray = array_values(explode('.', $key)); $newConfigClass = array_shift($keyArray); } foreach ($oldConfigValues as $key => $value) { $keyArray = array_values(explode('.', $key)); $oldConfigClass = array_shift($keyArray); if (isset($newConfigValues[$key])) { continue; } else { if ($oldConfigClass === $newConfigClass && $oldConfigValues[$key] === 'true') { $newConfigValues[$key] = 'false'; } else { $newConfigValues[$key] = $oldConfigValues[$key]; } } } if (!is_null($editData)) { $faqConfig->update($newConfigValues); } $faqConfig->getAll(); } ?> <form id="config_list" name="config_list" method="post" action="?action=config&config_action=saveConfig"> <input type="hidden" name="csrf" value="<?= $currentToken ?>"> <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"> <h1 class="h2"> <i aria-hidden="true" class="fa fa-wrench"></i> <?= $PMF_LANG['ad_config_edit'] ?> </h1> <div class="btn-toolbar mb-2 mb-md-0"> <div class="btn-group mr-2"> <button class="btn btn-sm btn-warning" type="reset"> <?= $PMF_LANG['ad_config_reset'] ?> </button> <button class="btn btn-sm btn-success" type="submit"> <?= $PMF_LANG['ad_config_save'] ?> </button> </div> </div> </div> <div class="row"> <div class="col-lg-12"> <ul class="nav nav-tabs" role="tablist"> <li role="presentation" class="nav-item"> <a href="#main" aria-controls="main" role="tab" data-toggle="tab" class="nav-link active"> <i aria-hidden="true" class="fa fa-home"></i> <?= $PMF_LANG['mainControlCenter'] ?> </a> </li> <li role="presentation" class="nav-item"> <a href="#records" aria-controls="records" role="tab" data-toggle="tab" class="nav-link"> <i aria-hidden="true" class="fa fa-th-list"></i> <?= $PMF_LANG['recordsControlCenter'] ?> </a> </li> <li role="presentation" class="nav-item"> <a href="#search" aria-controls="search" role="tab" data-toggle="tab" class="nav-link"> <i aria-hidden="true" class="fa fa-search"></i> <?= $PMF_LANG['searchControlCenter'] ?> </a> </li> <li role="presentation" class="nav-item"> <a href="#security" aria-controls="security" role="tab" data-toggle="tab" class="nav-link"> <i aria-hidden="true" class="fa fa-warning"></i> <?= $PMF_LANG['securityControlCenter'] ?> </a> </li> <li role="presentation" class="nav-item"> <a href="#spam" aria-controls="spam" role="tab" data-toggle="tab" class="nav-link"> <i aria-hidden="true" class="fa fa-thumbs-down"></i> <?= $PMF_LANG['spamControlCenter'] ?> </a> </li> <li role="presentation" class="nav-item"> <a href="#seo" aria-controls="seo" role="tab" data-toggle="tab" class="nav-link"> <i aria-hidden="true" class="fa fa-search"></i> <?= $PMF_LANG['seoCenter'] ?> </a> </li> <li role="presentation" class="nav-item"> <a href="#social" aria-controls="social" role="tab" data-toggle="tab" class="nav-link"> <i aria-hidden="true" class="fa fa-retweet"></i> <?= $PMF_LANG['socialNetworksControlCenter'] ?> </a> </li> <li role="presentation" class="nav-item"> <a href="#mail" aria-controls="mail" role="tab" data-toggle="tab" class="nav-link"> <i aria-hidden="true" class="fa fa-inbox"></i> <?= $PMF_LANG['mailControlCenter'] ?> </a> </li> <li role="presentation" class="nav-item"> <a href="#ldap" aria-controls="ldap" role="tab" data-toggle="tab" class="nav-link"> <i aria-hidden="true" class="fa fa-sitemap"></i> <?= 'LDAP' ?> </a> </li> <li role="presentation" class="nav-item"> <a href="#api" aria-controls="ldap" role="tab" data-toggle="tab" class="nav-link"> <i aria-hidden="true" class="fa fa-gears"></i> <?= 'API' ?> </a> </li> </ul> <div class="tab-content p-2 pt-4 pmf-configuration-panel"> <div role="tabpanel" class="tab-pane fade show active" id="main"></div> <div role="tabpanel" class="tab-pane fade" id="records"></div> <div role="tabpanel" class="tab-pane fade" id="search"></div> <div role="tabpanel" class="tab-pane fade" id="security"></div> <div role="tabpanel" class="tab-pane fade" id="spam"></div> <div role="tabpanel" class="tab-pane fade" id="seo"></div> <div role="tabpanel" class="tab-pane fade" id="social"></div> <div role="tabpanel" class="tab-pane fade" id="mail"></div> <div role="tabpanel" class="tab-pane fade" id="ldap"></div> <div role="tabpanel" class="tab-pane fade" id="api"></div> </div> </div> </div> </form> <script src="assets/js/configuration.js"></script> <?php } else { echo $PMF_LANG['err_NotAuth']; }
可能的原因及解决方法
1. Session存储路径权限问题
生产环境中PHP-FPM运行的用户(通常是www-data或nginx)可能没有Session存储目录的读写权限,导致每次请求都会创建新的Session,令牌自然不匹配。
- 解决方法:检查php.ini中
session.save_path配置的目录,确保该目录的所有者为PHP-FPM运行用户,权限设置为700。
2. PHP 8中session.cookie_secure配置问题
如果生产环境使用HTTPS,但$secureCookie变量值不正确,导致Session Cookie无法被正确携带,每次请求触发新Session创建。
- 解决方法:直接设置
ini_set('session.cookie_secure', '1');(确定HTTPS环境时),或者通过isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on'动态判断赋值给$secureCookie。
3. CSRF令牌生成/存储逻辑兼容性问题
PHP8对字符串、数组操作细节有变化,getCsrfTokenFromSession()方法可能存在令牌重复生成或未正确存储到Session的情况。
- 解决方法:检查
getCsrfTokenFromSession()实现:- 确保仅当Session中无令牌时才生成新令牌并存储
- 验证随机数生成等逻辑在PHP8中正常运行
- 确认令牌存储的Session键名无拼写错误
4. 反向代理/负载均衡导致Session丢失
生产环境的反向代理(如Nginx)或负载均衡可能未正确传递Session Cookie,或请求被分发到不同PHP-FPM实例,导致Session不一致。
- 解决方法:
- 确保反向代理配置传递
Host、X-Forwarded-For等头部,且PHP正确识别这些头部 - 负载均衡环境下配置共享Session存储(如Redis、Memcached),避免实例间Session隔离
- 确保反向代理配置传递
5. 输出缓冲导致Session Cookie未正确发送
如果session_start()之前有输出(空格、HTML标签等),PHP8中可能导致Session Cookie无法设置,每次请求创建新Session。
- 解决方法:检查代码开头是否有意外输出,开启输出缓冲(
ob_start()),确保session_start()在任何输出之前执行。
内容的提问来源于stack exchange,提问作者Code Buzzard
相关产品推荐
相关产品推荐

