目录
- osTicket 的 CSRF 工作原理详解
- 二、osTicket 采用什么 CSRF 方案
- 三、核心源码文件
- 四、CSRF 对象如何初始化
- 五、Token 保存在哪里
- 六、Token 是怎样生成的
- 七、什么时候生成 Token
- 八、Token 默认会过期吗
- 九、osTicket 如何向表单中加入 Token
- 十、提交表单时发送了什么
- 十一、osTicket 如何校验 Token
- 十二、底层比较逻辑
- 十三、员工登录的完整案例
- 十四、为什么登录失败后 Token 可能变化
- 十五、AJAX 请求如何携带 Token
- 十六、自定义 osTicket 页面如何正确加入 CSRF
- 十七、使用自定义 Token 字段名称
- 十八、没有 CSRF Token 会发生什么
- 十九、为什么攻击者不能直接获取 Token
- 二十、CSRF Token 不能防什么
- 二十一、为什么会出现 “Valid CSRF Token Required”
- 二十二、如何使用浏览器检查 osTicket CSRF
- 二十三、完整工作流程总结
- 二十四、开发 osTicket 插件时的推荐模板
osTicket 的 CSRF 工作原理详解
以下基于当前 osTicket v1.18.4 源码说明。v1.18.4 发布于 2026 年 6 月 17 日。(GitHub)
一、CSRF 是什么
CSRF 全称:
Cross-Site Request Forgery,跨站请求伪造
它利用的核心问题是:
浏览器访问某个网站时,会自动携带该网站的 Session Cookie。
例如,你已经登录了 osTicket 管理后台:
https://support.example.com/scp/
浏览器保存了 osTicket 的 Session Cookie:
Cookie: OSTSESSID=abc123...
随后你访问了攻击者的网站:
https://evil.example.com/
攻击者页面中隐藏了一个表单:
<form
action="https://support.example.com/scp/staff.php"
method="post"
id="attack-form"
>
<input type="hidden" name="do" value="delete">
<input type="hidden" name="id" value="15">
</form>
<script>
document.getElementById("attack-form").submit();
</script>
浏览器向 osTicket 发请求时,仍可能自动携带:
Cookie: OSTSESSID=abc123...
如果 osTicket 只检查 Session Cookie,就可能认为这是管理员本人操作,从而执行删除。
CSRF Token 的作用就是再增加一项验证:
Session Cookie 正确
+
CSRF Token 正确
=
请求才允许执行
攻击者虽然可以诱导浏览器发送请求,却通常无法读取 osTicket 页面里的 CSRF Token,因此无法构造完整有效的请求。(OWASP)
二、osTicket 采用什么 CSRF 方案
osTicket 使用的是典型的:
Synchronizer Token Pattern,服务器端 Session 同步 Token 模式
整体流程如下:
浏览器首次访问 osTicket
↓
osTicket 创建 Session
↓
生成随机 CSRF Token
↓
把 Token 保存到当前 Session
↓
把同一个 Token 放进 HTML 表单
↓
用户提交表单
↓
浏览器同时提交 Session Cookie 和 CSRF Token
↓
服务器取出 Session 中的 Token
↓
比较两个 Token
↓
一致:继续处理
不一致:拒绝请求
osTicket 默认使用:
Session Cookie 名称:OSTSESSID CSRF 参数名称:__CSRFToken__
osTicket 初始化时先启动名为 OSTSESSID 的 Session,然后创建名称为 __CSRFToken__ 的 CSRF 对象。(GitHub)
三、核心源码文件
osTicket 的 CSRF 核心实现主要位于:
include/class.csrf.php include/class.osticket.php
职责大致是:
class.csrf.php ├── 生成 Token ├── 保存 Token ├── 判断 Token 是否过期 ├── 验证 Token └── 生成隐藏表单字段 class.osticket.php ├── 初始化 Session ├── 初始化 CSRF 对象 ├── 从 POST 或请求头读取 Token ├── 调用 CSRF 类验证 └── 验证失败时记录系统日志
四、CSRF 对象如何初始化
include/class.osticket.php 中的核心逻辑可以简化为:
if (!defined('SESSION_SESSID')) {
define('SESSION_SESSID', 'OSTSESSID');
}
$this->session = osTicketSession::start(
SESSION_SESSID,
SESSION_TTL,
$this->isUpgradePending()
);
$this->csrf = new CSRF('__CSRFToken__');
它完成了两件事。
1. 启动 Session
浏览器获得类似 Cookie:
Set-Cookie: OSTSESSID=s4n1k0...;
后续请求会自动带上:
Cookie: OSTSESSID=s4n1k0...
2. 创建 CSRF 对象
new CSRF('__CSRFToken__');
意味着默认表单字段名称是:
__CSRFToken__
最终 HTML 中会看到:
<input
type="hidden"
name="__CSRFToken__"
value="7ac96b336bd3d11fe2e8d120f949a2550fab1234"
/>
五、Token 保存在哪里
CSRF 构造函数中有以下核心代码:
function __construct($name='__CSRFToken__', $timeout=0) {
$this->name = $name;
$this->timeout = $timeout;
$this->csrf = &$_SESSION['csrf'];
}
这里:
$this->csrf = &$_SESSION['csrf'];
表示 $this->csrf 是对当前 Session 中 csrf 数据的引用。
Session 里的数据概念上类似:
$_SESSION = [
'csrf' => [
'token' => '7ac96b336bd3d11fe2e8d120f949a2550fab1234',
'time' => 1783638000,
],
'_staff' => [
// 当前登录员工信息
],
];
所以 CSRF Token 并不是简单存在浏览器 Cookie 里,而是:
浏览器 Cookie
└── OSTSESSID:Session 标识
服务器 Session
└── csrf.token:真正的 CSRF Token
需要注意,class.csrf.php 文件顶部的旧注释写着 Token“不存储在 Session”,但 v1.18.4 的实际代码明确引用了 $_SESSION['csrf']。理解运行机制时,应以实际代码为准。(GitHub)
六、Token 是怎样生成的
核心代码:
function rotate() {
$this->csrf['token'] =
sha1(session_id() . Crypto::random(16) . SECRET_SALT);
$this->csrf['time'] = time();
}
可以抽象成:
CSRF Token =
SHA1(
当前 Session ID
+ 16 字节随机数据
+ 系统 SECRET_SALT
)
例如:
Session ID: f83d7c1a2e9... 随机数据: random-16-bytes SECRET_SALT: 系统安装时使用的秘密盐值
拼接以后计算 SHA-1:
sha1(
'f83d7c1a2e9...'
. $randomBytes
. $secretSalt
);
最终得到长度为 40 个十六进制字符的 Token:
7ac96b336bd3d11fe2e8d120f949a2550fab1234
生成逻辑包含三部分:
| 组成 | 作用 |
|---|---|
session_id() |
将 Token 与当前 Session 关联 |
Crypto::random(16) |
提供不可预测的随机性 |
SECRET_SALT |
加入服务端秘密值 |
sha1() |
将内容整理为固定长度字符串 |
其安全性主要依靠随机值和服务端秘密,而不是依靠 SHA-1 的抗碰撞能力。
Token 的生成、保存和隐藏字段输出代码都位于 include/class.csrf.php。(GitHub)
七、什么时候生成 Token
Token 通常不是每次调用都重新生成。
核心逻辑:
function getToken() {
if (
(!is_array($this->csrf) || !$this->csrf['token'])
|| $this->isExpired()
) {
$this->rotate();
} else {
$this->csrf['time'] = time();
}
return $this->csrf['token'];
}
流程可以理解为:
调用 getToken()
↓
Session 中有 Token 吗?
├── 没有:生成一个新 Token
└── 有
↓
过期了吗?
├── 是:生成新 Token
└── 否:继续使用原 Token
因此,同一 Session 中访问多个普通表单时,通常可以看到相同 Token:
工单回复表单:Token A 个人资料表单:Token A 新建工单表单:Token A
但是某些重要流程会主动调用:
$ost->getCSRF()->rotate();
这时会生成 Token B,旧 Token A 立即失效。
八、Token 默认会过期吗
构造函数默认参数是:
$timeout = 0
过期判断:
function isExpired() {
return (
$this->timeout
&& (time() - $this->csrf['time']) > $this->timeout
);
}
因为默认:
$this->timeout = 0;
所以默认情况下,CSRF Token 本身没有独立的超时时间。
它通常随着以下情况失效:
Session 失效 Session 被清理 用户退出登录 Session ID 变化 程序主动 rotate() 服务器端 Session 数据丢失
也就是说:
默认情况下,CSRF Token 的有效生命周期主要依赖 osTicket Session 生命周期,而不是单独的 CSRF 超时参数。
九、osTicket 如何向表单中加入 Token
1. 底层方法
function getFormInput($name='') {
if (!$name) {
$name = $this->name;
}
return sprintf(
'<input type="hidden" name="%s" value="%s" />',
$name,
$this->getToken()
);
}
输出类似:
<input
type="hidden"
name="__CSRFToken__"
value="7ac96b336bd3d11fe2e8d120f949a2550fab1234"
/>
2. 全局辅助函数
osTicket 还定义了:
function csrf_token() {
global $ost;
if ($ost && $ost->getCSRF()) {
echo $ost->getCSRFFormInput();
}
}
所以开发表单时,通常直接写:
<form method="post" action="profile.php">
<?php csrf_token(); ?>
<input type="text" name="name">
<button type="submit">保存</button>
</form>
浏览器最终获得:
<form method="post" action="profile.php">
<input
type="hidden"
name="__CSRFToken__"
value="7ac96b336bd3d11fe2e8d120f949a2550fab1234"
>
<input type="text" name="name">
<button type="submit">保存</button>
</form>
osTicket 员工登录表单就是在 <form> 开始位置调用 csrf_token()。(GitHub)
十、提交表单时发送了什么
假设表单如下:
<form method="post" action="profile.php">
<?php csrf_token(); ?>
<input type="text" name="name" value="Albert">
<button type="submit">保存</button>
</form>
浏览器发送:
POST /scp/profile.php HTTP/1.1 Host: support.example.com Cookie: OSTSESSID=f83d7c1a2e9... Content-Type: application/x-www-form-urlencoded __CSRFToken__=7ac96b336bd3d11fe2e8d120f949a2550fab1234 &name=Albert
请求里包含两项关键数据:
Cookie: OSTSESSID=f83d7c1a2e9... POST: __CSRFToken__=7ac96b336bd3...
服务器根据 OSTSESSID 找到 Session:
$_SESSION['csrf']['token']
然后将其与 POST 中的:
$_POST['__CSRFToken__']
进行比较。
十一、osTicket 如何校验 Token
include/class.osticket.php 中的核心代码:
function checkCSRFToken($name=false, $rotate=false) {
$name = $name ?: $this->getCSRF()->getTokenName();
$token =
$_POST[$name]
?: $_SERVER['HTTP_X_CSRFTOKEN'];
if ($token && $this->validateCSRFToken($token)) {
if ($rotate) {
$this->getCSRF()->rotate();
}
return true;
}
$msg = sprintf(
__('Invalid CSRF token [%1$s] on %2$s'),
Format::htmlchars(Format::sanitize($token)),
THISPAGE
);
$this->logWarning(
__('Invalid CSRF Token') . ' ' . $name,
$msg,
false
);
return false;
}
它会从两个地方查找 Token:
$_POST['__CSRFToken__']
或者 HTTP 请求头:
X-CSRFToken: 7ac96b336bd3...
完整顺序是:
POST 参数 __CSRFToken__
↓
如果不存在,再读取 X-CSRFToken 请求头
↓
调用 validateCSRFToken()
↓
Token 正确:返回 true
Token 错误:记录警告日志并返回 false
这使普通 HTML 表单和 AJAX 请求都能使用 CSRF 防护。(GitHub)
十二、底层比较逻辑
class.csrf.php 中:
function validateToken($token) {
return (
$token
&& trim($token) == $this->getToken()
&& !$this->isExpired()
);
}
等价于:
$submittedToken = trim($token);
$sessionToken = $this->getToken();
if (
$submittedToken !== ''
&& $submittedToken == $sessionToken
&& !$this->isExpired()
) {
return true;
}
return false;
校验条件包括:
1. 客户端确实提交了 Token 2. 去除首尾空格后与服务器 Token 相同 3. Token 没有过期
任意条件不满足,就会校验失败。
十三、员工登录的完整案例
osTicket 员工登录地址通常是:
/scp/login.php
第一步:访问登录页面
浏览器:
GET /scp/login.php HTTP/1.1 Host: support.example.com
服务器创建 Session:
OSTSESSID=SESSION_A
并生成 Token:
TOKEN_A
返回页面:
<form action="login.php" method="post">
<input
type="hidden"
name="__CSRFToken__"
value="TOKEN_A"
>
<input type="text" name="userid">
<input type="password" name="passwd">
</form>
第二步:提交用户名密码
浏览器发送:
POST /scp/login.php HTTP/1.1 Cookie: OSTSESSID=SESSION_A __CSRFToken__=TOKEN_A &userid=admin &passwd=123456
第三步:服务器读取 Session
服务器根据:
OSTSESSID=SESSION_A
取得:
$_SESSION['csrf']['token'] = 'TOKEN_A';
第四步:比较
请求 Token:TOKEN_A Session Token:TOKEN_A 结果:一致
于是继续验证用户名和密码。
如果不一致:
请求 Token:TOKEN_X Session Token:TOKEN_A 结果:不一致
osTicket 会提示:
Valid CSRF Token Required
员工登录处理程序会先调用 $ost->checkCSRFToken(),失败时将错误信息写入 Session 并重定向回登录页。(GitHub)
十四、为什么登录失败后 Token 可能变化
在员工普通登录失败后,osTicket 会执行:
$ost->getCSRF()->rotate();
这意味着:
原 Token:TOKEN_A
登录失败
↓
生成新 Token:TOKEN_B
↓
TOKEN_A 失效
目的之一是限制旧登录请求被重复提交。
例如攻击者截获了旧的登录请求:
__CSRFToken__=TOKEN_A &userid=admin &passwd=guess1
当服务端已经把 Token 更新为 TOKEN_B 后,再次重放 TOKEN_A:
请求 Token:TOKEN_A Session Token:TOKEN_B
校验失败。
客户端登录入口也明确在 POST 校验通过后主动轮换 Token,使原 Token 不能再次用于下一次登录尝试。(GitHub)
但需要注意:
Token 是否每次请求后轮换,由具体处理入口决定,不是所有 osTicket 表单都自动使用一次性 Token。
checkCSRFToken() 本身提供了 $rotate 参数:
$ost->checkCSRFToken(false, true);
也可以由业务代码在验证后手动执行:
$ost->getCSRF()->rotate();
十五、AJAX 请求如何携带 Token
osTicket 支持通过以下请求头发送 Token:
X-CSRFToken: TOKEN_A
PHP 页面输出 Token
<script>
const csrfToken = <?php
echo json_encode($ost->getCSRFToken());
?>;
</script>
使用 fetch 提交
fetch('ajax.php/custom/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
ticketId: 1001,
status: 'closed'
})
});
服务器端:
if (!$ost->checkCSRFToken()) {
Http::response(400, __('Valid CSRF Token Required'));
}
PHP 会把:
X-CSRFToken
映射为:
$_SERVER['HTTP_X_CSRFTOKEN']
所以 osTicket 的代码可以读取:
$token = $_SERVER['HTTP_X_CSRFTOKEN'];
源码注释也明确说明,普通表单可使用隐藏字段,AJAX 可使用 X-CSRFToken 请求头。(GitHub)
十六、自定义 osTicket 页面如何正确加入 CSRF
假设你新增一个页面:
/scp/custom-settings.php
用于修改自定义配置。
完整示例
<?php
require_once('staff.inc.php');
if (!defined('OSTSCPINC')) {
define('OSTSCPINC', true);
}
$errors = [];
$msg = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!$ost->checkCSRFToken()) {
Http::response(400, __('Valid CSRF Token Required'));
}
$value = trim($_POST['custom_value'] ?? '');
if ($value === '') {
$errors['custom_value'] = '配置值不能为空';
} else {
// 在这里保存配置
// CustomConfig::save($value);
$msg = '保存成功';
}
}
require(STAFFINC_DIR . 'header.inc.php');
?>
<h2>自定义配置</h2>
<?php if ($msg): ?>
<div id="msg_notice">
<?php echo Format::htmlchars($msg); ?>
</div>
<?php endif; ?>
<?php if (!empty($errors)): ?>
<div id="msg_error">
配置保存失败,请检查输入内容。
</div>
<?php endif; ?>
<form method="post" action="custom-settings.php">
<?php csrf_token(); ?>
<label for="custom_value">配置值</label>
<input
id="custom_value"
name="custom_value"
type="text"
value="<?php
echo Format::htmlchars(
$_POST['custom_value'] ?? ''
);
?>"
>
<?php if (isset($errors['custom_value'])): ?>
<div class="error">
<?php echo Format::htmlchars(
$errors['custom_value']
); ?>
</div>
<?php endif; ?>
<button type="submit">保存</button>
</form>
<?php
require(STAFFINC_DIR . 'footer.inc.php');
核心只有两部分。
表单中生成:
<?php csrf_token(); ?>
服务端校验:
if (!$ost->checkCSRFToken()) {
Http::response(400, __('Valid CSRF Token Required'));
}
十七、使用自定义 Token 字段名称
默认名称是:
__CSRFToken__
也可以使用自定义名称。
表单部分
<?php
echo $ost
->getCSRF()
->getFormInput('custom_csrf_token');
?>
输出:
<input
type="hidden"
name="custom_csrf_token"
value="TOKEN_A"
/>
服务端校验
if (!$ost->checkCSRFToken('custom_csrf_token')) {
Http::response(400, 'CSRF Token 校验失败');
}
不过在 osTicket 插件和自定义页面中,通常没有必要修改默认名称,直接使用:
csrf_token();
更符合原生代码习惯。
十八、没有 CSRF Token 会发生什么
假设直接使用 curl 提交:
curl -X POST \ https://support.example.com/scp/login.php \ -d "userid=admin&passwd=123456"
请求里没有:
__CSRFToken__
checkCSRFToken() 得到:
$token = null;
于是返回:
false
最终出现:
Valid CSRF Token Required
即使带了 Session Cookie,也仍然失败:
curl -X POST \ https://support.example.com/scp/login.php \ -H "Cookie: OSTSESSID=abc123" \ -d "userid=admin&passwd=123456"
因为有效请求需要同时具有:
正确 Session Cookie 正确 CSRF Token
十九、为什么攻击者不能直接获取 Token
假设攻击者页面位于:
https://evil.example.com
osTicket 位于:
https://support.example.com
攻击者可以创建跨站表单并诱导浏览器提交:
<form action="https://support.example.com/scp/profile.php">
但攻击者通常不能通过 JavaScript直接读取:
https://support.example.com/scp/profile.php
页面内容,因此无法知道隐藏字段:
<input name="__CSRFToken__" value="TOKEN_A">
攻击者只能猜:
<input
name="__CSRFToken__"
value="some-guessed-token"
>
服务器比较:
攻击者提交:some-guessed-token Session 保存:TOKEN_A
自然不一致。
二十、CSRF Token 不能防什么
1. 不能代替身份认证
CSRF Token 只能证明:
请求很可能来自服务器生成的合法页面。
它不能证明用户是谁。
因此仍然必须检查:
用户是否已登录 是否是员工 是否是管理员 是否有相关权限
正确顺序通常是:
if (!$thisstaff) {
Http::response(401, 'Authentication required');
}
if (!$thisstaff->hasPerm(...)) {
Http::response(403, 'Permission denied');
}
if (!$ost->checkCSRFToken()) {
Http::response(400, 'Valid CSRF Token Required');
}
2. 不能防止 XSS
如果 osTicket 页面存在 XSS,恶意脚本运行在 osTicket 自己的域名下,就可能读取:
document.querySelector(
'input[name="__CSRFToken__"]'
).value;
然后使用正确 Token 发请求。
因此:
CSRF 防护 ≠ XSS 防护
CSRF Token 和输出转义、CSP、输入过滤必须共同使用。
3. 不能替代业务权限验证
即使 Token 正确,也不能直接认为用户可以删除工单。
还必须检查:
$thisstaff->hasPerm(Ticket::PERM_DELETE)
CSRF 检查解决的是:
“这个请求是不是被第三方网站伪造的?”
权限检查解决的是:
“当前员工有没有权力执行这个操作?”
二十一、为什么会出现 “Valid CSRF Token Required”
这类错误本质上说明:
浏览器提交的 Token 和 当前 Session 中的 Token 没有对应上
常见原因如下。
1. Session Cookie 没有成功保存
浏览器提交表单时没有携带:
OSTSESSID
服务器会认为这是新 Session,于是生成新 Token:
页面 Token:TOKEN_A 服务器新 Session Token:TOKEN_B
校验失败。
需要在浏览器开发者工具中检查:
Application → Cookies → OSTSESSID
以及请求头:
Cookie: OSTSESSID=...
2. 页面打开太久,Session 已过期
例如:
09:00 打开编辑页面,Token 为 A 12:00 Session 已过期 12:10 点击保存
服务器创建新 Session,并得到 Token B:
浏览器提交:Token A 当前 Session:Token B
解决方式通常是刷新页面后重新提交。
3. 多个标签页中的 Token 被轮换
例如:
标签页 1:Token A 标签页 2:Token A
标签页 1 执行某个会 rotate() 的操作:
Session Token 更新为 Token B
标签页 2 仍提交 Token A:
Token A != Token B
于是失败。
这在登录、2FA 或主动轮换 Token 的流程中更容易出现。
4. 反向代理导致 Session 不稳定
例如架构:
浏览器 ↓ Nginx / Apache 反向代理 ↓ osTicket
如果以下配置不一致,可能导致 Cookie 或 Session 异常:
HTTP 与 HTTPS 不一致 Host 头错误 Cookie Path 不正确 Cookie Domain 不正确 代理前后 URL 不一致 负载均衡节点没有共享 Session
典型问题:
用户访问: https://support.example.com 后端认为: http://192.168.1.20/osticket
此时需要正确传递:
proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr;
并确保所有节点共享相同 Session 存储或启用粘性会话。历史上,osTicket 的相关问题报告也多次涉及 Session Cookie、反向代理和 Session 数据读取失败。(GitHub)
5. 页面缓存了旧 Token
如果 CDN、Nginx 或浏览器缓存了包含 Token 的 HTML:
<input name="__CSRFToken__" value="OLD_TOKEN">
但当前服务器 Session 中已经是:
NEW_TOKEN
提交就会失败。
因此登录页、后台页面和带有用户 Session 的页面不应被公共缓存。
可设置:
Cache-Control: no-store, no-cache, must-revalidate Pragma: no-cache
特别要避免 CDN 缓存:
/scp/* /login.php /tickets.php
6. 自定义表单漏掉 csrf_token()
错误代码:
<form method="post">
<input name="name">
<button type="submit">保存</button>
</form>
服务端却执行:
$ost->checkCSRFToken();
自然会失败。
正确代码:
<form method="post">
<?php csrf_token(); ?>
<input name="name">
<button type="submit">保存</button>
</form>
7. AJAX 没有提交 Token
错误:
fetch('/scp/ajax.php/custom/update', {
method: 'POST',
body: JSON.stringify(data)
});
正确:
fetch('/scp/ajax.php/custom/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify(data)
});
二十二、如何使用浏览器检查 osTicket CSRF
打开浏览器开发者工具。
第一步:检查页面表单
在 Elements 中搜索:
__CSRFToken__
应该看到:
<input
type="hidden"
name="__CSRFToken__"
value="..."
>
第二步:检查 POST 请求
在:
Network → 请求 → Payload
应该看到:
__CSRFToken__: 7ac96b...
第三步:检查 Cookie
在:
Network → 请求 → Headers → Request Headers
检查:
Cookie: OSTSESSID=...
第四步:对比页面和请求 Token
正常情况:
页面 Token:7ac96b... 请求 Token:7ac96b...
第五步:检查服务器日志
checkCSRFToken() 校验失败时会记录:
Invalid CSRF Token __CSRFToken__
并包括类似:
Invalid CSRF token [提交的值] on 当前页面
相关日志可以在 osTicket 管理后台系统日志中查看。
二十三、完整工作流程总结
以员工修改个人资料为例:
1. 员工访问 profile.php
↓
2. 浏览器携带 OSTSESSID
↓
3. osTicket 根据 OSTSESSID 恢复 Session
↓
4. CSRF 对象绑定 $_SESSION['csrf']
↓
5. 如果没有 Token,则调用 rotate()
↓
6. 生成:
SHA1(session_id + random + SECRET_SALT)
↓
7. csrf_token() 输出隐藏字段:
__CSRFToken__=TOKEN_A
↓
8. 员工填写表单并提交
↓
9. 浏览器发送:
Cookie: OSTSESSID=SESSION_A
POST: __CSRFToken__=TOKEN_A
↓
10. osTicket 根据 SESSION_A 读取 Session Token
↓
11. 比较:
POST TOKEN_A == Session TOKEN_A
↓
12. 相等则继续检查登录状态和权限
↓
13. 执行资料修改
失败流程:
POST Token 缺失或不匹配
↓
checkCSRFToken() 返回 false
↓
记录 Invalid CSRF Token 日志
↓
返回 400、重定向或显示:
Valid CSRF Token Required
二十四、开发 osTicket 插件时的推荐模板
<?php
require_once('staff.inc.php');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 1. 验证登录身份
if (!$thisstaff || !$thisstaff->isValid()) {
Http::response(401, 'Authentication required');
}
// 2. 验证权限
if (!$thisstaff->isAdmin()) {
Http::response(403, 'Permission denied');
}
// 3. 验证 CSRF
if (!$ost->checkCSRFToken()) {
Http::response(
400,
__('Valid CSRF Token Required')
);
}
// 4. 校验业务参数
$name = trim($_POST['name'] ?? '');
if ($name === '') {
Http::response(422, 'Name is required');
}
// 5. 执行业务操作
// save($name);
Http::redirect('custom.php?saved=1');
}
?>
<form method="post" action="custom.php">
<?php csrf_token(); ?>
<input
type="text"
name="name"
required
>
<button type="submit">
保存
</button>
</form>
记住三个层次不能混为一谈:
Session / 登录认证
→ 你是谁
权限验证
→ 你能做什么
CSRF 验证
→ 这个请求是否来自合法页面流程
osTicket 的 CSRF 核心可以浓缩成一句话:
服务端为当前
OSTSESSIDSession 保存一个不可预测的 Token,通过csrf_token()将其放入表单,提交时由checkCSRFToken()对比 POST 字段或X-CSRFToken请求头;只有请求值与当前 Session Token 匹配,业务操作才继续执行。