附件读取统一走 media.php 代理,真实地址不再暴露
无论附件存在本机 uploads/ 还是 OSS 私有桶,前台给浏览器的地址现在完全一致: media.php?k=<对象键>。界面上不再出现 uploads/ 真实路径,也不出现任何 bucket / endpoint / 签名痕迹。 - lib/attach.php:新增 fb_attach_proxy_url(),fb_attach_url() 与 fb_attach_thumb_url() 的本机分支一并改走代理(地址不落库、渲染现算, 对历史附件立即生效,无需迁移)。 - uploads/ 整目录禁止 Web 直读:附件只能经 media.php 出,真实物理路径 即便被猜到也取不到,同时绕不开 media.php 的可见性鉴权。 - 规则版本化自动升级:.htaccess 内埋 `# fb-local-guard: vN`, 旧版规则在任意请求时由 PHP(目录属主)整份重写。之所以不能靠部署覆盖: 部署账号对 uploads/ 无写权限,只有程序自己能改。 - 随包 uploads/.htaccess 改为程序产出的逐字节副本,并加断言防漂移 (否则线上会被自动升级覆盖,随包那份形同虚设)。 - 顺带修复真实源码泄漏:config.php.bak_<时间戳> 这类「非 .php 结尾、 但含源码」的备份文件此前可被公开下载。加固根 .htaccess: 去掉正则结尾锚点、补 .php[.\-_] 兜底、点名拒绝 config.php / oss.sts.php。 - media.php / upload.php / admin.php / README.md:文档与响应头同步。 验证:php -l 19 文件 0 失败;主回归 25/25、附件专项 22/22、安装向导 60/60、 统一代理渲染 7/7、反向验证 V1-V4 全被抓住;线上冒烟 30/30、 线上统一代理真机 13/13(OSS 私有桶 + 内网 endpoint 下上传→代理读回→ Range 206→伪造键 403)。
This commit is contained in:
+63
-17
@@ -38,7 +38,28 @@ function fb_local_dir() {
|
||||
* 确保本机附件目录存在,并补齐安全防护文件(.htaccess / index.html)。
|
||||
* ⚠️ 目录被删掉后由 PHP 重建时,.htaccess 不会自动回来 —— 必须在这里补写,
|
||||
* 否则「禁列目录 + 禁执行脚本」的防护就丢了。
|
||||
*
|
||||
* ⚠️⚠️ 【版本化升级】只判「文件在不在」是不够的:老站点上早已存在的
|
||||
* 旧版 .htaccess(允许直读)会一直留着,新规则永远写不进去。
|
||||
* 所以规则串里埋一行 `# fb-local-guard: vN`,每次确保时比对版本号,
|
||||
* 低于当前版本就整份重写。改规则时务必把 FB_LOCAL_GUARD_VER 一起 +1。
|
||||
*/
|
||||
define('FB_LOCAL_GUARD_VER', 2);
|
||||
|
||||
function fb_local_guard_rule() {
|
||||
return "# 本机附件目录(由程序自动生成)\n"
|
||||
. "# fb-local-guard: v" . FB_LOCAL_GUARD_VER . "\n"
|
||||
. "# ★ 一律禁止 Web 直接访问:附件只走 media.php 代理,真实路径不暴露\n"
|
||||
. "Options -Indexes\n\n"
|
||||
. "<IfModule mod_authz_core.c>\n Require all denied\n</IfModule>\n"
|
||||
. "<IfModule !mod_authz_core.c>\n Order allow,deny\n Deny from all\n</IfModule>\n\n"
|
||||
. "<FilesMatch \"\\.(php|php[0-9]|phtml|pht|phar|pl|py|cgi|sh|htaccess)$\">\n"
|
||||
. " <IfModule mod_authz_core.c>\n Require all denied\n </IfModule>\n"
|
||||
. " <IfModule !mod_authz_core.c>\n Order allow,deny\n Deny from all\n </IfModule>\n"
|
||||
. "</FilesMatch>\n";
|
||||
}
|
||||
|
||||
/** 确保目录存在并补齐防护文件(规则版本见上方 FB_LOCAL_GUARD_VER) */
|
||||
function fb_local_ensure_dir() {
|
||||
$dir = fb_local_dir();
|
||||
if (!is_dir($dir)) {
|
||||
@@ -48,15 +69,8 @@ function fb_local_ensure_dir() {
|
||||
}
|
||||
|
||||
$ht = $dir . '/.htaccess';
|
||||
if (!is_file($ht)) {
|
||||
$rule = "# 本机附件目录(由程序自动生成)\n"
|
||||
. "# 允许直接读取图片 / 视频,但禁止列出目录、禁止执行任何脚本\n"
|
||||
. "Options -Indexes\n\n"
|
||||
. "<FilesMatch \"\\.(php|php[0-9]|phtml|pht|phar|pl|py|cgi|sh|htaccess)$\">\n"
|
||||
. " <IfModule mod_authz_core.c>\n Require all denied\n </IfModule>\n"
|
||||
. " <IfModule !mod_authz_core.c>\n Order allow,deny\n Deny from all\n </IfModule>\n"
|
||||
. "</FilesMatch>\n";
|
||||
@file_put_contents($ht, $rule);
|
||||
if (!is_file($ht) || fb_local_guard_stale($ht)) {
|
||||
@file_put_contents($ht, fb_local_guard_rule());
|
||||
@chmod($ht, 0644);
|
||||
}
|
||||
|
||||
@@ -68,6 +82,19 @@ function fb_local_ensure_dir() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 现存的 .htaccess 是不是旧版(需要升级)。
|
||||
* 判据:拿不到 `fb-local-guard: vN` 标记,或标记里的 N < FB_LOCAL_GUARD_VER
|
||||
* → 都算过期,整份重写(旧文件没有标记,正是最需要升级的那种)。
|
||||
* 只在目录可写时才重写,避免不可写环境下每次都白试一遍。
|
||||
*/
|
||||
function fb_local_guard_stale($ht) {
|
||||
$body = @file_get_contents($ht, false, null, 0, 4096);
|
||||
if ($body === false) { return false; }
|
||||
if (strpos($body, '# fb-local-guard: v' . FB_LOCAL_GUARD_VER) !== false) { return false; }
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 本机附件是否可写 */
|
||||
function fb_local_writable() {
|
||||
fb_local_ensure_dir();
|
||||
@@ -130,17 +157,36 @@ function fb_local_path_of($key) {
|
||||
return fb_local_dir() . '/' . ltrim($rel, '/');
|
||||
}
|
||||
|
||||
/** 附件的访问地址(自动按 key 前缀分流) */
|
||||
function fb_attach_url($key) {
|
||||
if (fb_attach_is_local($key)) {
|
||||
return fb_base_url() . '/uploads/' . substr(ltrim((string)$key, '/'), 6);
|
||||
}
|
||||
return fb_oss_url($key);
|
||||
/**
|
||||
* 附件的【唯一对外地址】—— 一律走本站 media.php 代理。
|
||||
* ------------------------------------------------------------------
|
||||
* ★ 无论附件存在本机 uploads/ 还是阿里云 OSS,前台给浏览器的都是这一个形态:
|
||||
* media.php?k=<对象键>
|
||||
* 两个目的:
|
||||
* ① 路径统一 —— 存储方式(local / oss)对页面与前端 JS 完全透明,
|
||||
* 换存储方式不会产生两套 URL 逻辑、旧的本地附件也不会失效;
|
||||
* ② 不暴露真实地址 —— 本机模式下不再直接给出 uploads/2026/09/xxx.jpg
|
||||
* (避免被猜目录、被批量抓取),OSS 模式下更不会出现任何 bucket / endpoint 痕迹。
|
||||
* 真实的物理位置只由服务器在 media.php 内部按 key 前缀解析。
|
||||
* ⚠️ 数据库里只存对象键(oss_key),地址是渲染时现算的 ——
|
||||
* 所以本改动对【历史附件】同样立即生效,无需迁移数据。
|
||||
*/
|
||||
function fb_attach_proxy_url($key) {
|
||||
return fb_base_url() . '/media.php?k=' . rawurlencode(ltrim((string)$key, '/'));
|
||||
}
|
||||
|
||||
/** 附件的展示地址(缩略图;本机存储没有图片处理,直接给原图) */
|
||||
/** 附件的访问地址(原图;统一走 media.php 代理,与存储方式无关) */
|
||||
function fb_attach_url($key) {
|
||||
return fb_attach_proxy_url($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 附件的展示地址(缩略图;同样统一走 media.php 代理)
|
||||
* · OSS:借 OSS 图片处理在服务端缩放后再转发,省下行流量;
|
||||
* · 本机:没有图片处理能力,直接给原图(仍是 media.php 代理地址)。
|
||||
*/
|
||||
function fb_attach_thumb_url($key, $w = 640, $mime = '') {
|
||||
if (fb_attach_is_local($key)) { return fb_attach_url($key); }
|
||||
if (fb_attach_is_local($key)) { return fb_attach_proxy_url($key); }
|
||||
return fb_oss_thumb_url($key, $w, $mime);
|
||||
}
|
||||
|
||||
|
||||
@@ -132,4 +132,11 @@ if (PHP_SAPI !== 'cli' && session_status() !== PHP_SESSION_ACTIVE) {
|
||||
// install.php 在真正提交安装时会自己显式调用 fb_db()。
|
||||
if (fb_installed()) {
|
||||
fb_db();
|
||||
// 顺手确保 uploads/ 目录与「禁止直读」规则在位且为最新版(local / oss 都需要)。
|
||||
// ⚠️ 为什么放在这里:老站点上早已存在的旧版 .htaccess(曾允许直读)不会自己更新,
|
||||
// 而部署账号通常对 uploads/ 无写权限 —— 只有 PHP(目录属主)能改。
|
||||
// 函数内部只在「文件缺失或版本标记过期」时才写盘,命中后每次请求几乎零开销。
|
||||
if (function_exists('fb_local_ensure_dir')) {
|
||||
fb_local_ensure_dir();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user