把路径参数填成 ../secret.txt,服务端拼出来的路径就跑到了网站根目录外面。
Demo 里朴素版读到 200+泄露,加了 realpath 校验之后一律 403。
只做字符串过滤挡不住编码过的斜杠。
上一节是换行成了结构,这一节是 .. 成了结构——文件系统里「上一级」这个语义,被用户输入借走了。
一、.. 是文件系统的语法
「按名字取文件」这个动作,代码里通常写成 os.path.join(根目录, 用户给的名字)。名字正常时没问题。
但名字可以是 ../secret.txt。
.. 在文件系统里表示「上一级目录」,所以拼出来的路径自动往上跑了一层——跑到网站根目录外面去了。
多写几个 .. 就能往上跑更多层,一直到根。
所以这个漏洞的严重程度取决于进程的权限:如果服务以 root 跑,它能读到整台机器上的任何可读文件。
这也解释了为什么「网站根目录之外不能放敏感文件」是条硬规矩——就算有遍历漏洞,也读不到东西。
二、实测:五种写法,两种穿透
Demo 里五种输入:正常的 pages/about.txt、../secret.txt、../../secret.txt、
编码斜杠 ..%2fsecret.txt、....//secret.txt。
朴素版的结果是 200/200+泄露/404/200+泄露/404。
第二行和第四行都读到了根目录外的 secret.txt,正文里那句数据库口令直接就出来了。
第三行 404 是因为往上跑了两层,已经出了整个临时目录,文件不存在了。
第五行 404 是因为在本机(Windows)上 ....// 被当成了一个普通文件名。
第五行我不替它编结论:换个系统、换个服务器,解析结果可能就不一样。能确定的是它不该被信任。
三、校验要放在 realpath 之后
修正版的做法是:先 realpath 把路径归一化(所有 .. 和符号链接都展开成真实路径),
再用 commonpath 判断它是不是还在根目录下面。不是就回 403。
五种输入在修正版下是 200/403/403/403/403。正常的照常,想跑出去的一律拒绝。
为什么不能只做字符串过滤?
看第四行:..%2f 在过滤函数眼里是 ..%2f(不匹配 ../),在文件系统眼里是 ../。
解析器不一样,结果就不一样——所以校验必须放在解析之后。
还有一处细节:commonpath 在跨盘符、UNC 路径时会抛异常,代码里必须兜住,并且按「外部」处理。
这是那种「漏一个 except 就变成漏洞」的地方。
四、动手 Demo:五种路径,两种穿透
临时建一个网站目录,里面放一个正常文件,上一层放一个「数据库口令」。 两个服务端:一个直接拼路径,一个先归一化再比对;各喂五种输入。
full = os.path.realpath(os.path.join(WEB, name))
base = os.path.realpath(WEB)
try:
inside = os.path.commonpath([full, base]) == base
except ValueError: # 跨盘符、UNC 路径
inside = False
if not inside:
return 403
实跑输出:
--------------------------------------------------------------------
目录遍历:三个点加斜杠怎么就出了根目录
--------------------------------------------------------------------
手法 请求的参数 朴素版 加了路径校验
正常 pages/about.txt 200 200
上一级 ../secret.txt 200+泄露 403
两层上一级 ../../secret.txt 404 403
编码过的斜杠 ..%2fsecret.txt 200+泄露 403
四点加斜杠 ....//secret.txt 404 403
第一行的朴素版是 200,正常。第二行也是 200——但拿到的是**站点根目录外面**的 secret.txt,正文里就写着那句口令。
泄露成功的一条:`../secret.txt` → 拼出来的路径 …\www\../secret.txt
泄露成功的一条:`..%2fsecret.txt` → 拼出来的路径 …\www\../secret.txt
站点根目录 …/www
泄露的文件 …/secret.txt(在 www 的上一层)
校验怎么做的 realpath 归一化之后再比公共前缀
关键在 `realpath` 这一步:它把所有 `..` 和符号链接都展开成真实路径,然后才有得比。
只做字符串过滤(比如拦掉 `../`)是不够的:`..%2f` 这种编码过的斜杠,过滤函数看它是 `..%2f`,系统解出来是 `../`——上面第四行就是这么绕过去的。
朴素版第二行和第四行都是 200+泄露,正文里就是那句口令。
修正版这四行全是 403,只有第一行的正常文件还是 200。
编码斜杠那一行最能说明问题:..%2f 在字符串上完全不像 ../,但解出来就是 ../。
所以过滤字符串的思路一开始就错了。
../../secret.txt 在朴素版是 404,不是因为它被拦住了,是因为它跑得太远、
文件不存在——别把 404 当安全。
第五行 ....//secret.txt 两次都没成,我在正文里如实写了「本机没逃出去,我不替它编结论」。
30 秒实验
- 在 Linux 上重跑一遍这个 Demo,看第五行会不会变成泄露。
- 试试符号链接:在网站目录里放一个指向
../的链接,看校验拦不拦得住。 - 想一想:为什么「网站根目录外不放敏感文件」能降低危害,但不能算修好?
常见误区
- 「把
../过滤掉就行了」——..%2f、....//这些写法都能绕过。 - 「回 404 说明拦住了」——也可能是路径跑太远、文件真不存在。
- 「这个接口只给内部用」——内部接口一样会被 SSRF、被日志泄露带出来。
自测三问(点开对答案)
1. 目录遍历的成因是什么? —— 用户可控的文件名里带 `..`,拼出来的路径跑到了根目录外面。 2. 为什么校验必须放在 realpath 之后? —— 编码、多重点、符号链接都会在解析时变样,只有归一化之后才能可靠比较。 3. 为什么说 404 不等于安全? —— 可能是路径跑得太远、文件本来就不存在,而不是被拦住了。能读文件,下一步就是让服务端去包含一个文件——那就可能变成执行。
- ☐ (本节完,下一节 11.2.7 远程文件包含漏洞)