Nginx alias路径映射:四个必踩的坑

Nginx 里 aliash 和 root 长得像,坑却完全不同。这篇文章从一个真实的 404 排查说起,把这两个指令的差异、路径穿越的风险、软链接的解析方式,以及几类最容易出事的配置写法一次讲清楚。如果你手上正好有按目录映射的静态资源、上传目录、下载站或者多版本共存的站点,下面这些内容大概都会用得上。

一、从一次 404 说起

某天站点的用户说下载链接全部失效,但服务器上文件明明在。检查发现配置是这样写的:

location /download/ {
    alias /data/files/;
}

这条配置本身没问题:访问 /download/a.zip 会映射到 /data/files/a.zip。问题出在后面又加了一条规则,把新文件放进了 /data/files/2026/ 子目录,同时给下载链接加了个前缀 /download/files/,实际请求变成了 /download/files/a.zip。此时 Nginx 拼出来的路径是 /data/files/files/a.zip,多了一层,自然 404。

换成 root 写法的站点则不会出现这种困惑,因为 root 的语义是「把整个 URI 拼到根目录后面」,多一层前缀就一定多一层目录,行为可预测。alias 的语义是「把匹配到的 location 前缀替换成指定路径」,一旦 location 和实际目录结构对不上,就会拼出意料之外的路径。

二、root 与 alias 的路径计算规则

一句话记住区别:

  • root:最终路径 = root 值 + 完整 URI。
  • alias:最终路径 = alias 值 + 去掉 location 前缀后的剩余部分。

举例,两种写法能达到同样效果:

# 写法 A:root
location /static/ {
    root /var/www/site;
}
# /static/img/a.png  ->  /var/www/site/static/img/a.png

# 写法 B:alias
location /static/ {
    alias /var/www/site/static/;
}
# /static/img/a.png  ->  /var/www/site/static/img/a.png

两者都能工作,但维护成本不同。用 root 时,目录结构必须和 URI 结构保持一致,好处是规则简单、复制粘贴不会错;用 alias 时,URI 和磁盘目录可以完全解耦,适合「目录改名但外链不能变」的场景,代价是尾斜杠必须严格对应。

三、尾斜杠:alias 的第一大坑

alias 结尾的斜杠漏掉一个,路径就少一层,这是最常见的配置错误:

location /img/ {
    alias /data/pic;     # 少了结尾斜杠
}
# /img/a.png -> /data/pica.png      ← 路径被粘在一起

注意这里不是 404,而是拼成了一个不存在的文件 pica.png。如果恰好存在同名文件,还会被错误地返回出去,反过来构成信息泄露。规则很简单:location 以斜杠结尾,alias 就必须以斜杠结尾。

另外 location 用了正则、且带捕获组时,alias 里必须引用捕获:

location ~ ^/user/(.+\.(?:jpg|png))$ {
    alias /data/avatars/$1;
}

正则 location 里用 alias 而不带 $1,Nginx 会直接把整条 URI 拼上去,结果必然找不到文件。这种写法在从 Apache 的 RewriteRule 迁移时特别容易出错,因为 Apache 的 $1 语义和 Nginx 不完全一样。

四、路径穿越:alias 的危险写法

如果 location 前缀不以斜杠结尾,Nginx 会做「最长前缀替换」,攻击者可以用 .. 跳出目标目录:

location /files {
    alias /data/files/;    # 注意 location 没有结尾斜杠
}
# 请求 /files../etc/passwd
# 替换后 -> /data/files/../etc/passwd -> /data/etc/passwd

这条链路是可以真正读到文件的,属于典型的信息泄露漏洞。修复只要一步:location 和 alias 同时以斜杠结尾。如果历史配置不方便改,至少加一条拦截:

if ($request_uri ~ "\.\.") {
    return 400;
}

但更推荐的做法是把 URI 规范化交给 Nginx 自己处理,而不是在后端语言里做字符串过滤。需要注意的是,Nginx 默认会对 URI 做一次规范化(合并 //、解析 ..),但被 URL 编码的 %2e%2e 在 alias 参与拼接时可能不会被二次解码,所以配置层的斜杠对齐才是根本修法。

五、软链接:disable_symlinks 的两档语义

蓝绿发布用软链接切换目录是很常见的做法,例如 /data/app -> /data/releases/20261101/。默认情况下 Nginx 完全跟随软链接,这没有问题;但如果你把上传目录或用户可写目录暴露出去,跟随软链接就可能被利用:攻击者上传一个指向 /etc 的软链接,再通过静态资源路径访问,就能读到系统文件。

Nginx 提供 disable_symlinks 指令,三档取值:

disable_symlinks off;                 # 默认,全部跟随
disable_symlinks on;                  # 只要路径中有软链接就拒绝
disable_symlinks if_not_owner from=$document_root;
  • off:全部跟随,适合发布目录这类可信路径。
  • on:路径里任何一段是软链接就返回 403,安全但会打断蓝绿切换。
  • if_not_owner from=路径:路径中的软链接,只要所属用户与 from 指定的文件不同就拒绝。这是折中方案,能拦住「别的用户放的软链接」,同时允许运维自己维护的发布链接。

实践建议:上传目录、缓存目录一律用 if_not_owner,发布目录保持 off。另外注意 disable_symlinks on 会带来额外的 stat 调用,在高并发静态资源场景下有一定性能开销,不建议给全站的静态目录开满。

六、几种常见误配与对应现象

下面这些现象在排查时经常被误判成「Nginx 坏了」,其实都是配置语义问题:

  • 静态资源 200 但内容是首页 HTML:常见于 try_files 兜底写成 /index.php 而 fastcgi 又接了 PHP 目录,或者 alias 路径不存在时被 error_page 重写到了首页。先看响应头 Content-Type。
  • 同一个文件有时 200 有时 404:多半是 location 用了正则、优先级和前缀 location 冲突,两个 location 命中不同的 alias。用 nginx -T 会把所有 include 的配置全量打印出来,比翻文件快得多。
  • 目录访问 403 但文件能访问:autoindex 关闭且没有默认 index 文件,属于正常行为,不是权限问题。
  • 中文文件名 404:URI 中的中文是百分号编码,如果后端脚本自己又解码一次并在 alias 场景下拼路径,编码会和磁盘文件名不匹配。统一由 Nginx 处理,脚本侧不要再解码。

七、用 nginx -T 做一次配置体检

遇到路径类问题时,先别急着猜测,把实际生效的配置打印出来逐条对照:

nginx -t              # 语法检查
nginx -T | grep -n "alias"     # 全量配置里的所有 alias
nginx -T | grep -n -A3 "location /download"

再看一次实际请求的完整路径,可以让 Nginx 把解析结果打到错误日志:定位期间把日志级别临时调到 debug,请求一次后立刻调回,否则日志量会爆炸。

error_log /var/log/nginx/error.log debug;
# 复现后立即改回 warn 并 reload

排查完一定要记得把 debug 关掉。生产上留着一个 debug 日志,一周内就可能把磁盘写满。

八、把规则固化成配置模板

排查完一次路径问题,最有价值的产出不是「修好了」,而是把这次的判断固化成一份可以直接复制的新站配置模板。模板里应该固定几件事:静态目录一律用 root 并在末尾显式写斜杠;需要解耦 URI 与磁盘结构的下载或上传目录用 alias,且 location 与 alias 的结尾斜杠成对出现;所有用户可写目录显式声明 disable_symlinks if_not_owner from=$document_root;在 http 层统一加一条拦截,拒绝 URI 里出现两个连续的点。

location ~* /(upload|uploadfile|data/attach)/ {
    disable_symlinks if_not_owner from=$document_root;
    expires 7d;
    add_header Cache-Control "public";
}

location / {
    try_files $uri $uri/ /index.php?$query_string;
}

把这段存进版本库,新开站点时直接 include,比在每台机器上凭记忆重写一遍要可靠得多。团队里出过的每一个坑都值得变成模板里的一行,这是运维配置能长期保持干净的唯一办法。

同时建议在发布流程里加一道检查:每次改完 Nginx 配置,先在预发布环境跑一遍路径回归——列出站点里所有对外暴露的静态目录,各取一个文件请求一次,确认返回 200 且 Content-Type 正确。这个检查脚本只需要几十行,却能在上线前拦下绝大多数拼接错误。

九、小结:三条不踩坑的规矩

把上面这些坑压缩成三条可直接执行的规矩:第一,location 和 alias 的结尾斜杠必须成对出现,能不用 alias 的地方优先用 root;第二,上传目录和用户可写目录一律配 disable_symlinks if_not_owner,别让软链接成为跳出目录的梯子;第三,任何路径类故障先跑 nginx -T 看真实配置,再看 Content-Type 和错误日志,不要凭印象改。

路径问题之所以反复出现,是因为它从来不报错,只是安静地返回一个错误的文件或者一个 404。把这几条规矩固化成配置模板,下次新开站点时直接套用,能省掉大量不必要的排查时间。

Last modification:November 1st, 2026 at 07:25 pm

Leave a Comment