符号链接允许路径
默认情况下,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 行为。
配置
# 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 示例
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。
得到的规范路径会被存储为允许列表。重复项会被静默去重。
在请求时,路由层会对解析出的文件路径进行规范化,并验证它满足以下条件之一:
- 位于
DOCUMENT_ROOT内部,或 - 恰好等于允许列表中的某一条目(文件目标),或
- 以允许列表中的某一条目后接
/开头(目录目标)。
在静态文件服务路径中,同一检查会作为 TOCTOU 防护再运行一次——在路由缓存之后、任何读取系统调用之前。
黑名单
有一小组路径永远不能出现在 SYMLINK_ALLOW_PATHS 中;否则输入错误和理解偏差会极大地扩大攻击面。只要有任何条目解析到黑名单路径,服务器就会拒绝启动。
禁止作为精确匹配:
/ /etc /proc /sys /dev /var /home /tmp /root /usr /srv禁止作为前缀(条目位于以下某个目录之下):
/etc /proc /sys /dev /tmp /root /usr/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),因此运行时开销被摊薄掉了