回复框去掉管理员口令与处理状态;带 token 时昵称/邮箱锁定为只读

用户三个问题,本质是同一件事:token 就是身份凭证,那就不该再让表单声明身份。

一、去掉管理员口令 / 处理状态
  用户:「回复这个地方,下面有个管理员口令,还有处理状态,都不要填了,
        只保留回复内容,加上人机验证就可以了」
  - 删掉表单里这两块,删掉 admin-pw/admin-status 的 CSS 与 bindAdminToggle 的 JS
  - 管理员身份只认后台登录态(fb_is_admin()),不再从公开表单收口令
  - 状态流转只在后台详情页那排按钮里做

二、带 token 时昵称/邮箱锁定为只读
  用户:「如果他带着 Token 进来之后邮箱又让他改了,我们又没办法知道谁是谁了」
  - 有 token = 已证明是提交人本人,表单不再让填昵称/邮箱,
    改为显示只读的「回复身份」块(原昵称 + 原邮箱)

三、服务端强制(关键)
  🔴 前端设成 hidden 是不够的 —— hidden 可以被改:
     谁拿到链接、改个 email 提交,这条反馈的联系人就变成他的了。
     所以服务端在 isOwnerView 时**直接丢弃 POST 里的 name/email**,
     一律以库里那条帖子的 author_name / author_email 为准。
     身份校验必须在服务端做。

验证(这次确保请求真的通过人机验证,不是被验证码挡住造成的假通过):
  题目「2 乘以 7」答 14 → 跳转到 #r3,回复确实入库;
  提交的是 黑客改名 / attacker@evil.example,
  实际入库 name=jervis email=zeussun@msn.com —— 伪造的两个字段都被丢弃。
  另外:无 token 的普通访客仍可自由填昵称邮箱;全站已无「管理员口令」残留。
  测试回复已清理,reply_count 已同步。
This commit is contained in:
2026-11-03 10:02:57 +08:00
parent 8bcee46b8a
commit 8763e8e17f
5 changed files with 213 additions and 63 deletions
+50 -37
View File
@@ -61,7 +61,7 @@ if (!fb_thread_viewable($thread)) {
exit;
}
$statuses = fb_status_keys();
$err = '';
$atts = [];
$form = ['name' => '', 'email' => '', 'body' => '', 'captcha' => ''];
@@ -81,14 +81,27 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$form['name'] = trim(isset($_POST['name']) ? (string)$_POST['name'] : '');
$form['email'] = trim(isset($_POST['email']) ? (string)$_POST['email'] : '');
$form['body'] = trim(isset($_POST['body']) ? (string)$_POST['body'] : '');
/* 🔴 带 token(= 提交人本人)时,身份以库里那条帖子为准,**不采信 POST**。
* 前端已经把这两个字段设成 hidden 了,但 hidden 是可以被改的:
* 谁拿到链接、改个 email 提交,这条反馈的联系人就变成他的了。
* 身份校验必须在服务端做,不能只靠前端不给输入框。 */
if ($isOwnerView) {
$form['name'] = (string)$thread['author_name'];
$form['email'] = (string)$thread['author_email'];
}
$capInput = isset($_POST['captcha']) ? (string)$_POST['captcha'] : '';
$adminPw = isset($_POST['admin_password']) ? (string)$_POST['admin_password'] : '';
$hp = trim(isset($_POST['website']) ? (string)$_POST['website'] : '');
$t0 = isset($_POST['t0']) ? (int)$_POST['t0'] : 0;
list($attOk, $attList, $attErr) = fb_attach_parse(isset($_POST['attachments']) ? $_POST['attachments'] : '');
$isAdmin = ($adminPw !== '' && fb_admin_verify_password($adminPw));
/* 管理员身份只认「后台登录态」,不再从回复表单收口令。
* 用户 2026-11-03:「管理员口令、处理状态都不要填了,
* 用户进来之后就没有这些东西,只保留回复内容 + 人机验证」。
* 状态流转在后台详情页那排按钮里做,不该混在公开的回复框里。 */
$isAdmin = fb_is_admin();
if (!fb_csrf_ok(isset($_POST['csrf']) ? $_POST['csrf'] : '')) {
$err = '会话已过期,请重新提交。';
@@ -127,18 +140,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$replyId = (int)$pdo->lastInsertId();
fb_attach_save($pdo, $id, $replyId, $vList);
// 更新主题:回复数 +1、活跃时间;管理员可同时调整状态
$newStatus = null;
if ($isAdmin && isset($_POST['status']) && isset($statuses[(string)$_POST['status']])) {
$newStatus = (string)$_POST['status'];
}
if ($newStatus !== null) {
$upd = $pdo->prepare('UPDATE threads SET reply_count = reply_count + 1, updated_at = ?, status = ? WHERE id = ?');
$upd->execute([$now, $newStatus, $id]);
} else {
$upd = $pdo->prepare('UPDATE threads SET reply_count = reply_count + 1, updated_at = ? WHERE id = ?');
$upd->execute([$now, $id]);
}
/* 更新主题:回复数 +1、活跃时间。
* 回复框不再承担改状态的职责 —— 状态流转在后台详情页那排按钮里做。 */
$upd = $pdo->prepare('UPDATE threads SET reply_count = reply_count + 1, updated_at = ? WHERE id = ?');
$upd->execute([$now, $id]);
fb_rate_hit('reply');
// 发送通知邮件给发帖人
@@ -252,14 +257,36 @@ fb_layout_head(fb_excerpt($thread['title'], 50), ['software' => $thSwForColor]);
</div>
<div class="row2">
<label class="field">
<span>昵称 <b class="req">*</b></span>
<input type="text" name="name" maxlength="24" value="<?php echo e($form['name']); ?>" placeholder="怎么称呼您" required>
</label>
<label class="field" id="emailField">
<span>邮箱 <b class="req">*</b><i class="hint">仅用于接收回复通知,不会公开</i></span>
<input type="email" name="email" maxlength="120" value="<?php echo e($form['email']); ?>" placeholder="you@example.com" required>
</label>
<?php if ($isOwnerView): ?>
<?php /* 带 token 进来 = 已经证明是提交人本人。
*
* 🔴 昵称/邮箱在这里**只读**,理由:
* 1. token 就是身份凭证,再让他在表单里声明一次身份,自相矛盾;
* 2. 邮箱是唯一能联系到提交人的途径。如果能随手改,
* 谁拿到这条链接,填个自己的邮箱,后续所有通知就归他了 ——
* 等于把这条反馈「过户」给别人,我们再也找不到原作者。
* 要改联系方式,是另一件事(见下方说明),不该藏在回复框里顺手改掉。 */ ?>
<div class="field">
<span>回复身份</span>
<div class="reply-identity">
<span class="reply-identity-nm"><?php echo e((string)$thread['author_name']); ?></span>
<span class="reply-identity-mail"><?php echo e((string)$thread['author_email']); ?></span>
</div>
<i class="hint">这是您提交时用的身份。回复会以它显示,通知也会发到这个邮箱。</i>
</div>
<?php /* 表单仍要带这两个值,服务端才能落库;但不给用户改的机会 */ ?>
<input type="hidden" name="name" value="<?php echo e((string)$thread['author_name']); ?>">
<input type="hidden" name="email" value="<?php echo e((string)$thread['author_email']); ?>">
<?php else: ?>
<label class="field">
<span>昵称 <b class="req">*</b></span>
<input type="text" name="name" maxlength="24" value="<?php echo e($form['name']); ?>" placeholder="怎么称呼您" required>
</label>
<label class="field" id="emailField">
<span>邮箱 <b class="req">*</b><i class="hint">仅用于接收回复通知,不会公开</i></span>
<input type="email" name="email" maxlength="120" value="<?php echo e($form['email']); ?>" placeholder="you@example.com" required>
</label>
<?php endif; ?>
</div>
<label class="field">
@@ -280,20 +307,6 @@ fb_layout_head(fb_excerpt($thread['title'], 50), ['software' => $thSwForColor]);
</label>
</div>
<label class="field admin-pw">
<span>管理员口令 <i class="hint">选填;填写后将以「官方」身份回复</i></span>
<input type="password" name="admin_password" value="" autocomplete="off" placeholder="管理员专用">
</label>
<div class="field admin-status" hidden>
<span>处理状态 <i class="hint">仅管理员可见</i></span>
<select name="status">
<?php foreach ($statuses as $k => $v): ?>
<option value="<?php echo e($k); ?>" <?php echo $thread['status'] === $k ? 'selected' : ''; ?>><?php echo e($v); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-actions">
<button class="btn primary" type="submit">提交回复</button>
</div>