⭐ 推荐:社区规则条款 V1.0

键生成防可预测文件名:随机 hash 与上传安全

CLARA轻量论坛系统
CLARA轻量论坛系统 星耀SVIP星耀SVIP管理员管理员 黑卡会员黑卡会员
发布于 2026-10-11 20:01 ·2 浏览 ·0 回复
内容摘要

使用 random_bytes 等密码学安全随机源生成不可预测文件名,扩展名从白名单取,原文件名不参与磁盘路径,并配合目录分片与权限设置,防止上传文件被猜测访问。

学完这篇你能掌握一套「用随机 hash 生成不可预测文件名」的落地写法,并顺手把上传链路上几个最常见的窟窿一起堵上。

第一步:先搞清楚"可预测文件名"危险在哪

很多人上传时图省事,直接这么干:

$name = md5($file['name']) . '.' . $ext;   // 危险
$name = time() . '_' . rand(1000,9999);    // 危险
$name = uniqid() . '.' . $ext;             // 危险

问题在于:md5(原文件名) 完全可复现——攻击者拿常见文件名跑一遍字典就能命中;time() 秒级精度、rand() 未播种、uniqid() 基于微秒时间戳,都属于"可枚举"范畴。一旦文件名能被猜到,别人就能绕过列表页直接访问你的私有附件、用户上传的身份证照片、付费下载文件。

文件名应该是不可预测的随机值,而不是由任何输入推导出来的值。

第二步:用 random_bytes 生成随机种子

PHP 7 之后有密码学安全的随机源,直接用:

function randomFilename(string $ext): string {
    // 16 字节 = 128 位熵,碰撞概率可忽略
    return bin2hex(random_bytes(16)) . '.' . $ext;
}
// 形如:9f3c1a7b20e4d85c6b1f0a93e7d24c58.jpg

如果你想要固定 32 位长度的"hash 感",也可以:

$name = hash('sha256', random_bytes(32));  // 64 位十六进制

注意:不要用 rand()、mt_rand()、uniqid()、time() 单独作为唯一随机源。它们的输出空间小、可推算,属于典型的弱随机。

如果要保证同一次上传的重试幂等,可以拼一个业务前缀,但前缀里不要包含用户可控内容:substr($name, 0, 2) . '/' . $name。

第三步:扩展名必须从白名单里取,不能从原文件名取

这是最容易被忽略的一步。用户上传 shell.php.jpg,你 pathinfo 取到的是 jpg,看着没问题;但如果某处代码取的是 php,就出事了。稳妥做法是建立映射表,只允许已知安全后缀:

$allow = ['jpg'=>'jpg','jpeg'=>'jpg','png'=>'png','gif'=>'gif','webp'=>'webp','pdf'=>'pdf','zip'=>'zip'];
$ext   = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
if (!isset($allow[$ext])) { exit('类型不允许'); }
$saveName = bin2hex(random_bytes(16)) . '.' . $allow[$ext];

原文件名只存数据库用于展示和下载时还原,绝不参与磁盘路径拼接:

表字段:original_name(原始名)、stored_name(随机名)、path(相对路径)、size、mime、user_id

第四步:目录分片 + 落盘 + 权限

单个目录塞几十万个文件会让文件系统变慢,建议按日期分片:

$sub = date('Y/m');                       // uploads/2025/06/
$dir = __DIR__ . '/uploads/' . $sub;
if (!is_dir($dir)) mkdir($dir, 0755, true);
$target = $dir . '/' . $saveName;
move_uploaded_file($file['tmp_name'], $target);
chmod($target, 0644);

注意:目录权限给 0755、文件给 0644 就够了,别为了"省事"上 0777。上传目录永远不要让 Web 进程有执行权限。

第五步:给上传目录加一道"不可执行"保险

即使白名单写对了、二次校验做了,也建议在服务器层兜底。Nginx:

location ^~ /uploads/ {
    location ~* \.(php|php5|phtml|pht|phar)$ { deny all; }
}

Apache 则在 uploads 目录放一个 .htaccess:

php_flag engine off
<FilesMatch "\.(php|phtml|phar)$">
    Require all denied
</FilesMatch>

图片还要做内容二次校验,防止 .jpg 里塞进 PHP 代码:

if (!in_array($ext, ['pdf','zip'])) {
    if (@getimagesize($target) === false) { unlink($target); exit('不是有效图片'); }
}

第六步:在 Clara BBS 里对应的位置

Clara BBS 的上传安全基线本身就是"白名单 + 图片二次校验",运维时主要是把开关配对:

  • 后台「系统设置 → 上传」里,**"允许的扩展名"(管图片)和"附件允许的扩展名"(管附件)是两个独立字段**,改了一个不等于改了另一个。
  • PHP 的 upload_max_filesize 和 post_max_size 要大于实际文件体积,宝塔默认 2M,手机照片常超,建议调到 30M 以上。
  • uploads 目录需可写,否则上传失败。上传失败时页面会直接提示具体原因(超过 PHP 上限 / 白名单不符 / 目录不可写),照提示改即可。

注意:如果你自己写插件往上传流程里加钩子,务必沿用"随机存储名 + 原始名入库存展示"的分离思路,不要在插件里用原始文件名重新拼路径。

小结

  • 文件名只能来自 random_bytes() 这类密码学安全随机源,md5(原名)/time()/uniqid() 都不合格。
  • 扩展名从白名单映射取,不从原文件名取。
  • 原始名与存储名分离:磁盘用随机名,数据库存原名。
  • 目录按日期分片,权限 0755 / 0644。
  • 上传目录在服务器层禁止执行脚本,图片再做一次 getimagesize 内容校验。
  • Clara BBS 里注意"图片扩展名"和"附件扩展名"是两个独立字段,PHP 上传上限要单独调。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-794.html
转载请注明出处,版权归原作者所有。

全部回复 0

还没有回复,来抢沙发~