符号链接允许路径

默认情况下,OxPHP 会拒绝任何解析到规范 DOCUMENT_ROOT 之外路径的请求。DOCUMENT_ROOT 内部指向外部目录的符号链接会返回 404,并且路径解析会记录日志 Blocked request: resolved path escapes document root

这是正确的默认行为:它能阻止目录遍历、符号链接替换的 TOCTOU 攻击,以及意外暴露位于上一级目录的配置文件或密钥。但它同时也会拦截框架多年来一直在使用的合法模式:Laravel 的 php artisan storage:link、Symfony 资源包,以及挂载到多个容器中的共享上传卷。

SYMLINK_ALLOW_PATHS 就是显式的选择性启用机制:你在其中列出 DOCUMENT_ROOT 下的符号链接被允许解析到的文件系统路径。任何不在列表中的路径都保持严格的 404 行为。

配置

bash
# Absolute paths, comma-separated SYMLINK_ALLOW_PATHS=/var/www/storage,/opt/shared/assets # Relative paths resolve against DOCUMENT_ROOT SYMLINK_ALLOW_PATHS=../storage,../shared/uploads # Mixed SYMLINK_ALLOW_PATHS=/opt/shared/cdn,../storage/app/public

未设置时(默认),任何符号链接都无法离开 DOCUMENT_ROOT

Laravel 示例

bash
DOCUMENT_ROOT=/app/public SYMLINK_ALLOW_PATHS=../storage/app/public

之后 php artisan storage:link 会在项目内创建 public/storage -> ../storage/app/public。指向 /storage/<file> 的 URL 会通过该符号链接解析到 /app/storage/app/public/<file>,而它经过规范化后得到的路径正是允许列表所授权的。应用程序无需改动任何代码。

工作原理

在启动时,每一条目都会被解析:

  • 绝对路径条目——先与黑名单(见下文)比对,再经过 realpath(3)(即 std::fs::canonicalize)处理。如果 realpath 失败(目标不存在),服务器将拒绝启动。
  • 相对路径条目——与规范的 DOCUMENT_ROOT 拼接,再执行 realpath

得到的规范路径会被存储为允许列表。重复项会被静默去重。

在请求时,路由层会对解析出的文件路径进行规范化,并验证它满足以下条件之一:

  1. 位于 DOCUMENT_ROOT 内部,或
  2. 恰好等于允许列表中的某一条目(文件目标),或
  3. 以允许列表中的某一条目后接 / 开头(目录目标)。

在静态文件服务路径中,同一检查会作为 TOCTOU 防护再运行一次——在路由缓存之后、任何读取系统调用之前。

黑名单

有一小组路径永远不能出现在 SYMLINK_ALLOW_PATHS 中;否则输入错误和理解偏差会极大地扩大攻击面。只要有任何条目解析到黑名单路径,服务器就会拒绝启动。

禁止作为精确匹配:

text
/ /etc /proc /sys /dev /var /home /tmp /root /usr /srv

禁止作为前缀(条目位于以下某个目录之下):

text
/etc /proc /sys /dev /tmp /root /usr
Note

/var/home/srv 仅限精确匹配:裸的 /srv 会被拒绝,但 /srv/myapp/storage 是允许的,正如 /var/www/storage/home/<any>/... 也是允许的。条目会被检查两次:一次针对管理员提供的原始路径(这样 macOS 风格的 /etc -> /private/etc 就无法通过 realpath 把一个黑名单路径洗白),一次针对规范化后的形式(针对符号链接目标逃逸的纵深防御)。

黑名单本身是硬编码的;没有环境变量可以扩展它。默认值是能够捕获输入错误级别失误的保守最小集;需要更严格策略的管理员应在外层叠加(文件系统权限、容器挂载限制、AppArmor/SELinux 配置)。

失败模式

配置错误 结果
条目目标在磁盘上不存在 服务器拒绝启动,错误信息指明该条目和 canonicalize
条目匹配黑名单(原始或规范化形式) 服务器拒绝启动,错误信息指明该条目和相应的黑名单规则
空或仅含空白字符的环境变量 视为未设置——严格的默认行为
重复条目 规范化后静默去重
启动时尚不存在符号链接 允许列表已注册但处于惰性状态,直到符号链接出现为止;没有任何启动检查要求符号链接存在

安全说明

  • 允许列表是选择性启用的——当该变量未设置时,"不允许逃逸"这一安全默认得以保留
  • 条目在启动时被规范化,因此条目路径中的 .. 和中间符号链接会在存储前被折叠
  • 运行时规范化关闭了符号链接替换的 TOCTOU 窗口——被验证的路径正是被读取的路径
  • 文件目标精确匹配,目录目标按目录前缀匹配。将 /etc/passwd 这样的文件列入仍会被黑名单拒绝,但更一般地说:将单个文件 /opt/shared/license.key 列入并不会隐式授予对其同级文件的访问权限
  • 路径验证结果按请求的 URL 缓存(每个唯一 URL 在被逐出前只执行一次 realpath),因此运行时开销被摊薄掉了

另请参阅

  • PHP 拒绝路径 —— 在特定 URI 通配符下阻止 PHP 执行;与符号链接策略正交
  • 点路径拦截 —— 拒绝 .well-known 风格的遍历和点文件泄露
  • 可信代理 —— 针对 X-Forwarded-* 头的独立信任边界
  • 配置参考 —— 所有环境变量