文件读取功能几乎是所有Web应用都会涉及的基础模块,无论是下载附件、预览图片还是导出数据,底层都依赖文件路径解析与读取。但这一功能也是最容易被攻击者盯上的入口,SQL注入和路径遍历往往不是独立发生,而是通过参数构造形成组合拳。比如一个看似普通的文件ID参数,攻击者可以先尝试注入SQL语句获取数据库中的文件路径,再利用路径遍历符号跳出预期目录读取敏感文件。因此,防SQL注入和防路径遍历必须同步设计、同步编码、同步测试,任何只做单点防护的方案在实际攻防中都会留下明显短板。

先看SQL注入的防护。文件读取功能中SQL注入的高发点通常在文件元数据查询环节,例如根据文件ID查询存储路径、根据文件名模糊搜索、根据分类筛选文件列表。很多开发习惯用字符串拼接构造SQL,比如将用户传入的fileId直接拼进WHERE条件。攻击者提交“1 OR 1=1”这类载荷后,原本只应返回单条文件记录,结果却可能返回全表数据,进而拿到所有文件的存储路径和访问权限。更严重的是,如果数据库账号权限过高,攻击者还能通过联合查询读取系统表,或者利用堆叠注入执行额外的SQL命令。

最有效的防SQL注入手段是参数化查询,也就是预编译语句。无论后端使用Java的PreparedStatement、PHP的PDO还是Python的sqlite3,核心逻辑都是将SQL结构与数据分离,数据库驱动会把用户输入当作纯数据处理,不会解析其中的SQL关键字。以Java为例,正确写法如下:

String sql = "SELECT file_path, file_name FROM files WHERE file_id = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, fileId);
ResultSet rs = pstmt.executeQuery();

这种写法下,即使fileId的值为“1 OR 1=1”,数据库也只会把它当作字符串去匹配file_id字段,不会改变SQL语义。需要特别注意的是,参数化查询不能只用在查询语句上,插入、更新、删除操作同样要全覆盖。文件读取功能中常见的“记录下载日志”“更新最后访问时间”等操作,如果使用拼接SQL,一样存在注入风险。

如果项目中存在大量遗留代码无法立即改造为参数化查询,至少要增加输入校验和过滤作为过渡措施。但校验规则必须严格,不能只做简单的黑名单过滤。文件ID如果设计为数字,就强制校验是否为纯数字;如果设计为UUID,就校验格式是否符合UUID规范。对于文件名等字符串参数,建议采用白名单字符集,只允许字母、数字、下划线、连字符和点号,长度也要限制在合理范围。需要明确的是,输入过滤只能作为纵深防御的一层,不能替代参数化查询,因为黑名单过滤总有可能被绕过,比如大小写混写、双重编码、Unicode同形字等。

再谈路径遍历攻击。文件读取功能最常见的实现方式是根据用户提供的文件名或相对路径,在服务器指定目录下拼接出完整文件路径。如果用户输入没有经过严格校验,攻击者可以提交“../../etc/passwd”或“..%2f..%2fconfig.php”这样的载荷,让程序跳出预设的文件存储目录,读取服务器上的任意文件。路径遍历的危害取决于被读取文件的内容,可能是数据库配置文件、应用源码、系统账号信息,甚至是私钥文件。在云原生环境中,还可能出现读取挂载的Kubernetes服务账号令牌等情况。

路径遍历的防护核心是路径规范化与边界校验。程序拿到用户输入的文件名或相对路径后,首先要进行规范化处理,将路径中的“..”和“.”解析为实际的目录结构。不同语言有不同的处理方式,Java可以用Path.normalize(),Python可以用os.path.normpath(),Node.js可以用path.normalize()。规范化之后,再拼接基础目录,然后检查最终路径是否仍然位于基础目录之内。以Java为例:

String baseDir = "/var/app/files";
String userFile = request.getParameter("file");
Path basePath = Paths.get(baseDir).toAbsolutePath().normalize();
Path filePath = basePath.resolve(userFile).normalize();

if (!filePath.startsWith(basePath)) {
    throw new SecurityException("非法文件路径");
}

这段代码的关键在于先规范化再判断前缀。如果只做字符串前缀判断,攻击者可以用“/var/app/files/../../etc/passwd”绕过,因为字符串开头确实匹配了基础目录。规范化之后,“..”会被解析,最终路径变成“/var/app/etc/passwd”,此时再判断前缀就能发现路径已经逃逸出基础目录。还需要注意,Windows系统下路径分隔符是反斜杠,攻击者可能混用正斜杠和反斜杠绕过校验,因此最好同时处理两种分隔符,或者统一转换为正斜杠后再做判断。

另一个容易被忽略的细节是符号链接。即使程序做了严格的路径边界校验,如果基础目录内部存在指向外部目录的符号链接,攻击者仍然可能通过符号链接读取到目录之外的文件。例如基础目录下有一个“link”符号链接指向“/etc”,攻击者提交“link/passwd”就能读取系统密码文件。防护方法是使用Path.toRealPath()代替normalize(),toRealPath会解析所有符号链接,返回真实的物理路径。解析后再做前缀判断,就能发现符号链接指向的路径已经超出基础目录范围。不过toRealPath要求路径必须存在,对于不存在的文件需要先检查父目录的真实路径。

文件读取功能中,SQL注入和路径遍历还存在联动攻击的可能。攻击者先通过SQL注入获取数据库中存储的文件路径信息,然后利用路径遍历读取这些路径指向的文件。或者反过来,通过路径遍历读取应用配置文件,拿到数据库连接信息后直接连接数据库执行SQL操作。因此,在设计文件读取功能时,数据库账号权限要遵循最小权限原则,只授予必要的SELECT权限,并且限制可查询的表范围。文件存储目录要与应用代码目录、系统目录物理隔离,最好使用独立的磁盘分区或对象存储服务。

对于文件ID的设计,建议不要直接使用数据库自增ID作为文件访问标识。自增ID容易被枚举,攻击者可以遍历ID获取所有文件。更安全的做法是使用随机生成的UUID或带签名的令牌作为文件访问标识,服务端通过这个标识查询数据库获取真实存储路径,查询过程使用参数化SQL。这样即使攻击者拿到了文件访问标识,也无法推测其他文件的标识,更无法通过修改标识中的数字部分来遍历文件。如果文件访问标识中需要携带用户信息或权限信息,建议使用HMAC签名,防止攻击者篡改标识内容。

文件读取功能的日志记录也值得关注。很多应用会记录文件下载日志,包括文件名、文件路径、操作用户等信息。如果日志记录时使用了字符串拼接SQL,同样会引入注入风险。而且日志中如果记录了完整的文件路径,一旦日志文件被路径遍历读取,攻击者就能获得所有文件的存储位置。因此日志中的文件路径应该做脱敏处理,只记录文件名和文件ID,不记录完整的服务器本地路径。

在测试环节,针对文件读取功能的安全测试不能只停留在功能验证层面。SQL注入测试要覆盖所有文件查询参数,包括文件ID、文件名、分类ID、排序字段等。路径遍历测试要使用多种编码方式的载荷,包括URL编码、双重编码、Unicode编码、反斜杠变体等。测试用例中要包含“../”的多种组合深度,以及绝对路径、空字节截断等历史漏洞载荷。自动化扫描工具可以作为辅助,但不能完全依赖,因为很多路径遍历漏洞需要结合具体业务逻辑才能触发。

对于已经上线的文件读取功能,建议增加运行时防护措施。可以在应用层增加Web应用防火墙规则,检测请求参数中的路径遍历特征和SQL注入特征。但WAF规则同样存在绕过可能,只能作为应急手段。更有效的做法是在代码层面增加统一的文件读取封装函数,所有文件读取操作都必须经过这个函数,函数内部统一执行参数校验、路径规范化和边界检查。这样可以从架构上避免开发人员遗漏防护逻辑。

文件读取功能的安全防护本质上是一个系统性问题。SQL注入防护解决的是数据查询层面的安全,路径遍历防护解决的是文件系统访问层面的安全,两者缺一不可。开发人员需要在编码阶段就建立安全基线,而不是等安全测试发现问题后再打补丁。安全基线包括:所有SQL操作使用参数化查询、所有文件路径操作经过规范化与边界校验、数据库账号最小权限、文件访问标识不可预测、日志脱敏。只有把这些措施落实到每一行代码中,文件读取功能才能真正具备抵御攻击的能力。