直接结论:.env 文件可被 HTTP 访问是因 Web 服务器未配置拦截规则,非 ThinkPHP 问题;需在 Nginx server 块中前置添加 deny all 规则屏蔽 .env 等敏感文件,并将 runtime 目录移出 Web 根目录实现物理隔离。
直接结论:.env 文件一旦能被 HTTP 访问,数据库密码就等于贴在服务器门口——这不是 ThinkPHP 的锅,是 Nginx/Apache 配置漏了、部署结构错了、或 Git 误提交了。
因为 Web 服务器默认不拦截 .env。Nginx 不会自动拒绝 /.env$,Apache 也不会主动屏蔽点开头的文件,除非你显式加规则。常见现象是访问 https://yoursite.com/.env 直接返回明文内容,含 DB_PASSWORD、APP_KEY 等。
根本原因不是框架没加密,而是路径暴露 + 规则缺失。ThinkPHP 6+ 加载 .env 是通过 vlucas/phpdotenv 在 PHP 运行时读取文本,它本身不阻止 HTTP 请求。
curl -I https://yoursite.com/.env 看是否返回 200 OK 或 text/plain
location ~ .php$ 规则太宽,且没前置 location ~ /.env$,导致请求被当作静态文件下发.env 拦截规则这三行要写进站点 server 块内、所有 location / 和 location ~ .php$ 之前,顺序错就失效。
立即学习“PHP免费学习笔记(深入)”;
location ~ /.env$ { deny all; }location ~ /.(env|env.example|dist|lock|git|svn|hg|yml|yaml)$ { deny all; }location ~ ^/(app|config|runtime|common|extend|vendor)/ { deny all; }
注意细节:
. 中的点必须转义,写成 .,否则 /aenv 也会被误拦$ 必须保留,否则 /path/to/env.php 会被波及^/ 表示从 URI 根开始匹配,/(app|config)/ 会漏掉 /app(无尾斜杠)或 /app/ 子路径,而 ^/(app|config)/ 能覆盖全部nginx -t && nginx -s reload,别只改不重载硬编码 'password' => '123456' 等同于把钥匙焊死在门把手上。一旦 config/database.php 因服务器配置疏忽被解析或下载,密码立刻裸奔。
正确做法是彻底剥离,全靠环境变量注入:
.env 放项目根目录(与 app/、config/ 同级),**绝不能放 public/ 下**.env 内写 DB_PASSWORD='Zx9#kL2!mQp@'(单引号包裹,避免 shell 特殊字符干扰)config/database.php 中对应项写成 'password' => env('DB_PASSWORD', ''),第二个参数是 fallback,不能为空字符串以外的值,否则连接会静默失败public/index.php 中有 thinkinitializerEnv::init()(TP6+ 默认已有).env 后必须清缓存:php think clear:config 或删 runtime/config/ 下文件runtime/ 是 ThinkPHP 自动生成日志、缓存、模板编译文件的地方。如果它既在 Web 根目录下,又设了 777 权限,攻击者上传一个 shell.php 到 runtime/log/,就能直接执行。
最可靠的方式是物理隔离:
/var/www/myapp/(代码根),Web 服务器 root 指向 /var/www/myapp/public/
runtime/ 实际放在 /var/www/myapp/runtime/(与 public/ 同级),确保任何 URL 都无法映射到它config/app.php 中显式配置:'runtime_path' => '/var/www/myapp/runtime/',
is_writable('/var/www/myapp/runtime/') || die('runtime not writable');
最后提醒一句:环境变量优先级比 .env 高。如果系统已用 export DB_PASSWORD=xxx 设置过,.env 里的同名项会被跳过——验证时别只看文件,要用 getenv('DB_PASSWORD') 和 env('DB_PASSWORD') 一起查。