键生成防可预测文件名:随机 hash 与上传安全
使用 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 上传上限要单独调。
转载请注明出处,版权归原作者所有。
星耀SVIP
管理员
黑卡会员






