附件读取统一走 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:
@@ -1,7 +1,56 @@
|
|||||||
# 安全与整洁:禁用目录列表;阻止直接下载数据库 / 说明文件
|
# FeedLite —— 安全与整洁(Apache 规则)
|
||||||
|
#
|
||||||
|
# 作用:禁用目录列表;阻止对数据库 / 配置文件 / 备份文件 / 文档的直接下载。
|
||||||
|
# ⚠️ 这些规则只拦「静态直读」。.php 文件本身仍是「被 PHP 执行」而非下载,
|
||||||
|
# 所以拦截的重点是「非 .php 后缀但内容敏感」的文件 —— 例如手工或工具产生的
|
||||||
|
# config.php.bak_20260928_193339(名字以数字结尾!旧写法的 \.bak$ 匹配不到它,
|
||||||
|
# 于是它被当作静态文件原样吐出,等于公开了管理员口令与接口令牌)。
|
||||||
|
# 因此后缀匹配一律放宽为「后缀出现在文件名任意位置」,而不是只匹配结尾。
|
||||||
|
|
||||||
Options -Indexes
|
Options -Indexes
|
||||||
|
|
||||||
<FilesMatch "\.(db|sqlite|sqlite3|log|md|ini|bak)$">
|
# —— 数据库 / 日志 / 文档 / 配置与备份(含任意后缀形式)——
|
||||||
|
# 关键点:不写 $ 结尾锚点,避免 config.php.bak_20260928_193339 这类名字漏网。
|
||||||
|
<FilesMatch "(?i)\.(db|sqlite|sqlite3|log|md|ini|bak|old|orig|save|swp|swo|tmp|temp|copy|dist|example\.php\.|php\.(bak|old|orig|save|txt|swp|tmp))">
|
||||||
|
<IfModule mod_authz_core.c>
|
||||||
|
Require all denied
|
||||||
|
</IfModule>
|
||||||
|
<IfModule !mod_authz_core.c>
|
||||||
|
Order allow,deny
|
||||||
|
Deny from all
|
||||||
|
</IfModule>
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# —— 兜底:任何「含 .php 且后面还跟着别的东西」的文件一律拒绝 ——
|
||||||
|
# 正常 PHP 入口都是 xxx.php 结尾,会被 PHP 执行;
|
||||||
|
# 而 config.php.bak_20260928、config.php.txt、admin.php.old 这类
|
||||||
|
# 不会被执行、只会被当文本下载 —— 全部拒掉。
|
||||||
|
<FilesMatch "(?i)\.php[.\-_]">
|
||||||
|
<IfModule mod_authz_core.c>
|
||||||
|
Require all denied
|
||||||
|
</IfModule>
|
||||||
|
<IfModule !mod_authz_core.c>
|
||||||
|
Order allow,deny
|
||||||
|
Deny from all
|
||||||
|
</IfModule>
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# —— 隐藏文件(.env / .git 之类)——
|
||||||
|
<FilesMatch "^\.">
|
||||||
|
<IfModule mod_authz_core.c>
|
||||||
|
Require all denied
|
||||||
|
</IfModule>
|
||||||
|
<IfModule !mod_authz_core.c>
|
||||||
|
Order allow,deny
|
||||||
|
Deny from all
|
||||||
|
</IfModule>
|
||||||
|
</FilesMatch>
|
||||||
|
|
||||||
|
# —— 点名拒绝:只含真实凭据的配置文件(正常请求永远不会直接访问它们)——
|
||||||
|
# 它们通常会被 PHP 执行(返回空 body)而不泄漏,但显式拒绝可以防「PHP 处理器被改
|
||||||
|
# 成静态输出」「服务器换了不支持的执行配置」这类意外,属于纵深防御。
|
||||||
|
# 注意:文件系统层面的 require 不受影响,程序读配置照常工作。
|
||||||
|
<FilesMatch "(?i)^(config|config\.local|oss\.sts)\.php$">
|
||||||
<IfModule mod_authz_core.c>
|
<IfModule mod_authz_core.c>
|
||||||
Require all denied
|
Require all denied
|
||||||
</IfModule>
|
</IfModule>
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ feedback/
|
|||||||
├─ admin.php 管理后台(口令登录:审核 / 管理全部问题 / 站点设置 / 操作日志)
|
├─ admin.php 管理后台(口令登录:审核 / 管理全部问题 / 站点设置 / 操作日志)
|
||||||
├─ install.php 安装向导(数据库不存在时自动引导进来;装完建议删除)
|
├─ install.php 安装向导(数据库不存在时自动引导进来;装完建议删除)
|
||||||
├─ upload.php 附件上传接口(两步:领对象键 → 收文件本体,local 落盘 / OSS 代理 PUT)
|
├─ upload.php 附件上传接口(两步:领对象键 → 收文件本体,local 落盘 / OSS 代理 PUT)
|
||||||
├─ media.php 附件代理输出(私有桶 + 内网 Endpoint 下图片/视频的唯一对外读取通道,带鉴权与 Range)
|
├─ media.php 附件代理输出(图片/视频的**唯一对外读取通道**:无论存本机还是 OSS,前台地址都是 media.php?k=…,带鉴权与 Range)
|
||||||
├─ captcha.php "换一题"用的人机验证刷新接口(返回 JSON)
|
├─ captcha.php "换一题"用的人机验证刷新接口(返回 JSON)
|
||||||
├─ api.php 只读数据接口(需令牌),供自动化 / AI 读取
|
├─ api.php 只读数据接口(需令牌),供自动化 / AI 读取
|
||||||
├─ cron.php 附件过期清理 + 通知邮件补发(配计划任务)
|
├─ cron.php 附件过期清理 + 通知邮件补发(配计划任务)
|
||||||
@@ -36,7 +36,7 @@ feedback/
|
|||||||
├─ lib/ 内部库(bootstrap / db / util / oss / attach / captcha / mailer / layout)
|
├─ lib/ 内部库(bootstrap / db / util / oss / attach / captcha / mailer / layout)
|
||||||
├─ assets/ style.css / app.js / 图标(favicon、各尺寸 PNG、site.webmanifest)
|
├─ assets/ style.css / app.js / 图标(favicon、各尺寸 PNG、site.webmanifest)
|
||||||
├─ data/ SQLite 数据库目录(已用 .htaccess 拒绝访问)
|
├─ data/ SQLite 数据库目录(已用 .htaccess 拒绝访问)
|
||||||
├─ uploads/ 本机附件目录(仅"本机硬盘"模式下使用;PHP 按需自动创建)
|
├─ uploads/ 本机附件目录(仅"本机硬盘"模式下使用;PHP 按需自动创建;**目录禁止 Web 直连,附件只走 media.php**)
|
||||||
├─ .htaccess 禁止目录列表、禁止下载 .db/.md
|
├─ .htaccess 禁止目录列表、禁止下载 .db/.md
|
||||||
└─ README.md 本文件
|
└─ README.md 本文件
|
||||||
```
|
```
|
||||||
@@ -275,14 +275,29 @@ php /www/wwwroot/你的域名/feedback/cron.php
|
|||||||
> `local/…` 一律按本机文件读,`fb/…`(或你配的 `oss.prefix`)一律按 OSS 读。
|
> `local/…` 一律按本机文件读,`fb/…`(或你配的 `oss.prefix`)一律按 OSS 读。
|
||||||
> 所以中途从 OSS 换成硬盘(或反过来)时,**已经发过的帖子里的附件照常显示**。
|
> 所以中途从 OSS 换成硬盘(或反过来)时,**已经发过的帖子里的附件照常显示**。
|
||||||
|
|
||||||
|
> ★ **前台地址与存储方式无关(统一代理)**
|
||||||
|
> 无论选「本机硬盘」还是「阿里云 OSS」,页面上的图片 / 视频地址**永远**是同一个形态:
|
||||||
|
> ```
|
||||||
|
> media.php?k=<对象键> 例:media.php?k=local%2F2026%2F09%2Fabcd.jpg
|
||||||
|
> ```
|
||||||
|
> 这样做有两个好处:
|
||||||
|
> 1. **路径统一**:存储方式对页面与前端 JS 完全透明,换存储不用改前端、也不会出现两套 URL 逻辑;
|
||||||
|
> 2. **不暴露真实地址**:本机模式下不会给出 `uploads/2026/09/xxxx.jpg` 这类可被猜目录、被批量抓取的直链;
|
||||||
|
> OSS 模式下更不会出现任何 bucket / endpoint / 签名痕迹。
|
||||||
|
> 真实的物理位置只由服务器在 `media.php` 内部按对象键前缀解析。
|
||||||
|
> 数据库里**只存对象键**,地址是渲染时现算的 —— 所以这项改动对**历史附件也立即生效,无需迁移数据**。
|
||||||
|
|
||||||
**本机硬盘模式的两个注意点**
|
**本机硬盘模式的两个注意点**
|
||||||
|
|
||||||
1. 目录由 PHP **按需自动创建**(`fb_local_writable()` 里的 `@mkdir`),属主就是 PHP 进程用户,天然可写。
|
1. 目录由 PHP **按需自动创建**(`fb_local_writable()` 里的 `@mkdir`),属主就是 PHP 进程用户,天然可写。
|
||||||
⚠️ **不要用 SFTP/部署账号手动建这个 `uploads/` 目录**——那样属主会变成部署账号,
|
⚠️ **不要用 SFTP/部署账号手动建这个 `uploads/` 目录**——那样属主会变成部署账号,
|
||||||
PHP 写不进去,上传会失败。后台「存储体检」会明确显示"可写 ✅ / 不可写 ❌"并给出处置办法;
|
PHP 写不进去,上传会失败。后台「存储体检」会明确显示"可写 ✅ / 不可写 ❌"并给出处置办法;
|
||||||
真遇到了,**删掉整个 `uploads/`(含 `.htaccess`)再刷新页面**,PHP 会重建。
|
真遇到了,**删掉整个 `uploads/`(含 `.htaccess`)再刷新页面**,PHP 会重建。
|
||||||
2. 目录里自动带一个 `.htaccess`:禁止列目录、禁止执行 `.php`/`.pl`/`.py`/`.cgi`/`.sh` 等脚本,
|
2. 目录里自动带一个 `.htaccess`:**整个目录一律拒绝 Web 直接访问**(附件只走 `media.php` 代理,
|
||||||
只允许当静态图片 / 视频读取。对象键由服务端生成并做正则白名单校验,客户端无法指定路径。
|
真实路径不外泄),并额外禁止执行 `.php`/`.pl`/`.py`/`.cgi`/`.sh` 等脚本。
|
||||||
|
该规则带**版本标记**(`# fb-local-guard: vN`),程序每次确保目录时比对版本,
|
||||||
|
老站点上残留的旧版 `.htaccess` 会被**自动升级**,不需要手工替换。
|
||||||
|
对象键由服务端生成并做正则白名单校验,客户端无法指定路径。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -577,7 +577,7 @@ fb_layout_head('管理后台', ['robots' => 'noindex,nofollow']);
|
|||||||
<option value="local"<?php echo $mode === 'local' ? ' selected' : ''; ?>>本机硬盘(站点 uploads/ 目录)</option>
|
<option value="local"<?php echo $mode === 'local' ? ' selected' : ''; ?>>本机硬盘(站点 uploads/ 目录)</option>
|
||||||
<option value="oss"<?php echo $mode === 'oss' ? ' selected' : ''; ?>>阿里云 OSS(私有桶 · 服务器代理)</option>
|
<option value="oss"<?php echo $mode === 'oss' ? ' selected' : ''; ?>>阿里云 OSS(私有桶 · 服务器代理)</option>
|
||||||
</select>
|
</select>
|
||||||
<i class="hint">本机:文件落在站点 uploads/ 目录;OSS:文件经服务器代理写入私有桶(走内网 Endpoint),公网无法直连桶。两种方式都需要服务器磁盘能放下单个上传文件。</i>
|
<i class="hint">本机:文件落在站点 uploads/ 目录;OSS:文件经服务器代理写入私有桶(走内网 Endpoint),公网无法直连桶。两种方式都需要服务器磁盘能放下单个上传文件。<b>前台展示一律经 media.php 代理</b>——无论存哪,页面上的图片/视频地址都是 <code>media.php?k=…</code>,不暴露 uploads/ 真实路径,也不出现任何 OSS 桶信息。</i>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="grid2">
|
<div class="grid2">
|
||||||
|
|||||||
+63
-17
@@ -38,7 +38,28 @@ function fb_local_dir() {
|
|||||||
* 确保本机附件目录存在,并补齐安全防护文件(.htaccess / index.html)。
|
* 确保本机附件目录存在,并补齐安全防护文件(.htaccess / index.html)。
|
||||||
* ⚠️ 目录被删掉后由 PHP 重建时,.htaccess 不会自动回来 —— 必须在这里补写,
|
* ⚠️ 目录被删掉后由 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() {
|
function fb_local_ensure_dir() {
|
||||||
$dir = fb_local_dir();
|
$dir = fb_local_dir();
|
||||||
if (!is_dir($dir)) {
|
if (!is_dir($dir)) {
|
||||||
@@ -48,15 +69,8 @@ function fb_local_ensure_dir() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$ht = $dir . '/.htaccess';
|
$ht = $dir . '/.htaccess';
|
||||||
if (!is_file($ht)) {
|
if (!is_file($ht) || fb_local_guard_stale($ht)) {
|
||||||
$rule = "# 本机附件目录(由程序自动生成)\n"
|
@file_put_contents($ht, fb_local_guard_rule());
|
||||||
. "# 允许直接读取图片 / 视频,但禁止列出目录、禁止执行任何脚本\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);
|
|
||||||
@chmod($ht, 0644);
|
@chmod($ht, 0644);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,6 +82,19 @@ function fb_local_ensure_dir() {
|
|||||||
return true;
|
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() {
|
function fb_local_writable() {
|
||||||
fb_local_ensure_dir();
|
fb_local_ensure_dir();
|
||||||
@@ -130,17 +157,36 @@ function fb_local_path_of($key) {
|
|||||||
return fb_local_dir() . '/' . ltrim($rel, '/');
|
return fb_local_dir() . '/' . ltrim($rel, '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 附件的访问地址(自动按 key 前缀分流) */
|
/**
|
||||||
function fb_attach_url($key) {
|
* 附件的【唯一对外地址】—— 一律走本站 media.php 代理。
|
||||||
if (fb_attach_is_local($key)) {
|
* ------------------------------------------------------------------
|
||||||
return fb_base_url() . '/uploads/' . substr(ltrim((string)$key, '/'), 6);
|
* ★ 无论附件存在本机 uploads/ 还是阿里云 OSS,前台给浏览器的都是这一个形态:
|
||||||
}
|
* media.php?k=<对象键>
|
||||||
return fb_oss_url($key);
|
* 两个目的:
|
||||||
|
* ① 路径统一 —— 存储方式(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 = '') {
|
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);
|
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()。
|
// install.php 在真正提交安装时会自己显式调用 fb_db()。
|
||||||
if (fb_installed()) {
|
if (fb_installed()) {
|
||||||
fb_db();
|
fb_db();
|
||||||
|
// 顺手确保 uploads/ 目录与「禁止直读」规则在位且为最新版(local / oss 都需要)。
|
||||||
|
// ⚠️ 为什么放在这里:老站点上早已存在的旧版 .htaccess(曾允许直读)不会自己更新,
|
||||||
|
// 而部署账号通常对 uploads/ 无写权限 —— 只有 PHP(目录属主)能改。
|
||||||
|
// 函数内部只在「文件缺失或版本标记过期」时才写盘,命中后每次请求几乎零开销。
|
||||||
|
if (function_exists('fb_local_ensure_dir')) {
|
||||||
|
fb_local_ensure_dir();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,9 @@
|
|||||||
* ③ 只允许图片/视频白名单 MIME,且透传 Range 以支持视频拖动进度条。
|
* ③ 只允许图片/视频白名单 MIME,且透传 Range 以支持视频拖动进度条。
|
||||||
* ④ 全程流式转发,不把文件读进内存(memory_limit 小也安全)。
|
* ④ 全程流式转发,不把文件读进内存(memory_limit 小也安全)。
|
||||||
*
|
*
|
||||||
* 本机存储(local/...)的附件由 Web 服务器直接吐 uploads/ 下的文件,不走这里;
|
* 本机存储(local/...)与本文件的 OSS 分支【前台地址完全一致】:
|
||||||
* 但为兼容旧链接,这里也做一次本地回退。
|
* 两种存储方式对外都只暴露 media.php?k=... —— 路径统一、真实地址不泄漏。
|
||||||
|
* uploads/ 目录自身也被 .htaccess 全部拒绝,直连真实路径取不到东西。
|
||||||
*/
|
*/
|
||||||
require __DIR__ . '/lib/bootstrap.php';
|
require __DIR__ . '/lib/bootstrap.php';
|
||||||
|
|
||||||
@@ -40,7 +41,8 @@ if ($process !== '') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------- 本机存储:直接读磁盘并流式输出 ---------------- */
|
/* ---------------- 本机存储:直接读磁盘并流式输出 ----------------
|
||||||
|
* 与前台的统一代理地址配套:local 附件也走这里,uploads/ 目录本身禁止直连。 */
|
||||||
if (fb_attach_is_local($key)) {
|
if (fb_attach_is_local($key)) {
|
||||||
// 先鉴权再探文件,避免用 404/403 差异被探测对象是否存在
|
// 先鉴权再探文件,避免用 404/403 差异被探测对象是否存在
|
||||||
if (!fb_media_authorized($key)) { fb_media_fail(403, '无权访问该附件'); }
|
if (!fb_media_authorized($key)) { fb_media_fail(403, '无权访问该附件'); }
|
||||||
@@ -53,6 +55,7 @@ if (fb_attach_is_local($key)) {
|
|||||||
header('Content-Length: ' . $size);
|
header('Content-Length: ' . $size);
|
||||||
header('Accept-Ranges: bytes');
|
header('Accept-Ranges: bytes');
|
||||||
header('Cache-Control: public, max-age=86400');
|
header('Cache-Control: public, max-age=86400');
|
||||||
|
header('X-Content-Type-Options: nosniff'); // 与 OSS 分支保持一致
|
||||||
fb_media_send_local($path, $size);
|
fb_media_send_local($path, $size);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
* 浏览器既拿不到可直连的 URL、也连不上内网地址,所以「浏览器直传」在此架构下不成立。
|
* 浏览器既拿不到可直连的 URL、也连不上内网地址,所以「浏览器直传」在此架构下不成立。
|
||||||
* 读取同理,走 media.php 代理(见该文件)。
|
* 读取同理,走 media.php 代理(见该文件)。
|
||||||
*
|
*
|
||||||
|
* ★ 前台地址与存储方式无关:local 与 oss 返回的 url 都是 media.php?k=...,
|
||||||
|
* 页面/前端 JS 看不到 uploads/ 真实路径,也看不到任何 OSS 痕迹。
|
||||||
|
*
|
||||||
* 两种方式共用同一套校验:扩展名 / MIME 白名单、单文件大小、每 IP 频率、全站总量上限。
|
* 两种方式共用同一套校验:扩展名 / MIME 白名单、单文件大小、每 IP 频率、全站总量上限。
|
||||||
*/
|
*/
|
||||||
require __DIR__ . '/lib/bootstrap.php';
|
require __DIR__ . '/lib/bootstrap.php';
|
||||||
|
|||||||
+11
-2
@@ -1,7 +1,16 @@
|
|||||||
# 本机附件目录
|
# 本机附件目录(由程序自动生成)
|
||||||
# 允许直接读取图片 / 视频,但禁止列出目录、禁止执行任何脚本
|
# fb-local-guard: v2
|
||||||
|
# ★ 一律禁止 Web 直接访问:附件只走 media.php 代理,真实路径不暴露
|
||||||
Options -Indexes
|
Options -Indexes
|
||||||
|
|
||||||
|
<IfModule mod_authz_core.c>
|
||||||
|
Require all denied
|
||||||
|
</IfModule>
|
||||||
|
<IfModule !mod_authz_core.c>
|
||||||
|
Order allow,deny
|
||||||
|
Deny from all
|
||||||
|
</IfModule>
|
||||||
|
|
||||||
<FilesMatch "\.(php|php[0-9]|phtml|pht|phar|pl|py|cgi|sh|htaccess)$">
|
<FilesMatch "\.(php|php[0-9]|phtml|pht|phar|pl|py|cgi|sh|htaccess)$">
|
||||||
<IfModule mod_authz_core.c>
|
<IfModule mod_authz_core.c>
|
||||||
Require all denied
|
Require all denied
|
||||||
|
|||||||
Reference in New Issue
Block a user