You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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&amp;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。

如果生产环境使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 17:45:28