From b835afcd0489e2d4725231c4e62275b294940b3e Mon Sep 17 00:00:00 2001 From: Yongquan Date: Sun, 1 Nov 2026 15:07:53 +0800 Subject: [PATCH] =?UTF-8?q?data/uploads=20=E6=94=B9=E4=B8=BA=E7=94=B1=20PH?= =?UTF-8?q?P=20=E8=87=AA=E5=8A=A8=E5=88=9B=E5=BB=BA=E5=B9=B6=E8=87=AA?= =?UTF-8?q?=E8=A1=A5=E9=98=B2=E6=8A=A4=EF=BC=8C=E4=B8=8D=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E9=83=A8=E7=BD=B2=E8=B4=A6=E5=8F=B7=E5=BB=BA=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=20(#1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 问题 部署时用 btops 建 data/ 与 uploads/,属主成了 btops:btops,Web 用户 www 写不进去, 安装向导直接报「目录不可写」,还得请人用 root 手工 chown —— 这不该让用户干。 ## 修法:让 PHP 自己建 PHP 以 Web 用户身份运行,它建出来的目录属主天生就是 www,不需要任何 chown。 - 部署脚本不再创建 data/ uploads/(只建 assets/ lib/) - 首次访问时 fb_db_init() 自动 mkdir data/,并立刻补防护文件 - uploads/ 由 fb_local_ensure_dir() 同样自动建 + 补防护 - 安装向导的环境体检 mkdir 之后也立刻补防护,不留空窗期 ## 新增 data/ 防护文件自动生成 原先只有 uploads/ 有自动防护,data/ 没有 —— 数据库目录裸奔。 新增 fb_db_ensure_guard(),写 .htaccess(拒绝直读,禁 db/db-wal/db-shm/sqlite/php) + index.html 占位,带 `# fb-data-guard: vN` 版本标记,规则升级时整份重写。 ## 顺带确认 站点根 .htaccess 已有全局兜底(拒绝 .db/.log/.md/.bak 等直读 + Options -Indexes), 即使某个目录暂时缺 .htaccess,/data/ 和 /data/feedback.db 也是 403。 现在是双保险,不依赖单点。 验证(WSL 真实环境): - 全新安装 33/33、老基线 25/25、附件专项 22/22、安装向导 60/60 - data/ 与 uploads/ 均由 PHP 自动创建,属主即运行用户 - 两者 .htaccess 自动生成并带正确版本标记(v1 / v2) --- install.php | 8 ++++++++ lib/db.php | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/install.php b/install.php index 21ee7db..bd2ee2e 100644 --- a/install.php +++ b/install.php @@ -78,6 +78,14 @@ function fb_install_checks() { foreach ($dirs as $d => $why) { if (!is_dir($d)) { @mkdir($d, 0755, true); } + // 目录一建好就立刻补防护文件,不留"目录存在但无防护"的空窗期。 + // data/ 用 fb_db_ensure_guard(),uploads/ 用 fb_local_ensure_dir(), + // 两者都在被保护目录里写 .htaccess + index.html。 + if (basename($d) === 'data' && function_exists('fb_db_ensure_guard')) { + fb_db_ensure_guard($d); + } elseif (basename($d) === 'uploads' && function_exists('fb_local_ensure_dir')) { + fb_local_ensure_dir(); + } $w = is_dir($d) && is_writable($d); if (!$w) { $ok = false; } $shown = ($d === FB_ROOT) ? '(站点根)' : str_replace(FB_ROOT, '', $d); diff --git a/lib/db.php b/lib/db.php index df9d79b..17f8121 100644 --- a/lib/db.php +++ b/lib/db.php @@ -11,7 +11,11 @@ function fb_db_init($cfg) { $path = (string)$cfg['db_path']; $dir = dirname($path); + // 目录不存在就自己建:PHP 以 Web 用户身份运行,建出来的属主天然正确, + // 不需要部署账号预先建好、也不需要事后 chown。 if (!is_dir($dir)) { @mkdir($dir, 0755, true); } + // 每次进来都确保防护文件在位(函数内部有版本判断,命中后几乎零开销) + fb_db_ensure_guard($dir); $fresh = !file_exists($path); @@ -29,6 +33,51 @@ function fb_db_init($cfg) { return $pdo; } +/** + * 确保数据库目录存在防护文件(.htaccess 拒绝直读 + index.html 占位)。 + * + * 为什么不靠部署时上传:data/ 是可写目录,部署账号建它会让属主变成部署账号, + * Web 用户反而写不进去。让 PHP 自己建 + 自己补防护,属主永远正确。 + * 版本号机制与 uploads/ 一致:规则串里埋 `# fb-data-guard: vN`, + * 低于当前版本就整份重写,避免老站点留着没防护的旧文件。 + */ +define('FB_DATA_GUARD_VER', 1); + +function fb_data_guard_rule() { + return "# 数据库目录(由程序自动生成,请勿删除)\n" + . "# fb-data-guard: v" . FB_DATA_GUARD_VER . "\n" + . "# ★ 一律禁止 Web 直接访问:数据库文件绝不能被下载\n" + . "Options -Indexes\n\n" + . "\n Require all denied\n\n" + . "\n Order allow,deny\n Deny from all\n\n\n" + . "\n" + . " \n Require all denied\n \n" + . " \n Order allow,deny\n Deny from all\n \n" + . "\n"; +} + +function fb_db_ensure_guard($dir) { + if (!is_dir($dir)) { return false; } + $ht = $dir . '/.htaccess'; + $need = true; + if (is_file($ht)) { + $head = @file_get_contents($ht, false, null, 0, 512); + if ($head !== false && strpos($head, '# fb-data-guard: v' . FB_DATA_GUARD_VER) !== false) { + $need = false; + } + } + if ($need) { + @file_put_contents($ht, fb_data_guard_rule()); + @chmod($ht, 0644); + } + $idx = $dir . '/index.html'; + if (!is_file($idx)) { + @file_put_contents($idx, ''); + @chmod($idx, 0644); + } + return true; +} + /** 便捷访问器 */ function fb_db() { return fb_db_init(fb_cfg());