注入 - 路径遍历
路径遍历是另一种相当常见的注入漏洞。当 URI(无论是 URL、文件路径还是其他路径)的构建未能正确确保解析后的完整路径不超出预期路径的根目录时,往往就会发生此类漏洞。
值得注意的是,路径遍历实际上也可以被视为一种路径“注入”漏洞。
路径遍历漏洞的影响在很大程度上取决于遍历发生的环境以及所采取的整体加固措施。但在深入探讨之前,让我们通过一个简单的实际示例来看看这种漏洞究竟是怎么回事:
简要分析
假设您的应用程序中有一个用于提供文档的端点,例如合同模板或工作机会模板。这些文件(如 PDF)在您的应用程序中通常是静态的。
在这种情况下,您可能会使用类似以下的代码来根据请求获取文件:
let baseFolder = "/var/www/api/documents/";
let path = baseFolder + request.params.filename;
return file.read(path);
为了演示该漏洞是如何产生的,我们还需要知道应用程序的根目录在哪里。因此,在本例中,假设应用程序的根目录位于“/var/www/api/”。
我们已知应用程序接收一个“filename”参数,让我们看看几个输入示例及其结果:
请注意我们是如何利用“../”来遍历文件系统的。我们能够跳出通常存放 PDF 的“documents”文件夹,进入包含“shadow”文件的“/etc/”文件夹,而在 Linux 系统中,该文件包含密码哈希值。您可以想象,这显然是非常不安全的。
查看 URL 中的遍历
路径遍历的另一种变体可能发生在构建旨在与 API 交互的 URL 时。假设我们有一个包含以下方法的 API:
该 API 由另一个应用程序调用,例如在尝试获取订单信息时:
let apiBase = "https://my.api/api/v1";
let orderApi = apiBase + "/order/get";
let apiUrl = orderApi + request.params.orderId;
let response = http.get(apiUrl);
那么,根据用户提供的订单 ID,现在会发生什么呢?下方展示了基于所提供输入而实际调用的 URL。
规范化通常不在客户端完成(尽管也可以这样做),但 Web 服务器会将请求规范化为下方的格式。
在第二个示例的输入中,我们没有获取 ID 为“1”的订单,而是实际上调用了删除方法,这当然会导致订单被删除。
缓解措施
在讨论路径遍历时,既有直接的缓解措施,也有间接的防御技术,这些方法应当尽可能多地应用。首先,让我们看看如何处理路径。
直接缓解措施
在处理路径时,我们必须理解路径解析(或路径规范化)的过程及其重要性。
当你拥有像“/var/www/api/documents/../../../../etc/shadow”这样的路径时,它处于非规范化状态。如果你从文件系统中请求此路径,它会将其规范化为“/etc/shadow”。至关重要的是,不要尝试打开非规范化的路径。相反,你应该先规范化路径,验证它们是否仅指向预期的文件或文件夹,然后再进行读取。
let baseFolder = "/var/www/api/documents/";
let path = baseFolder + request.params.filename;
let resolvedPath = path.resolve(path);
if(!resolvedPath.startswith(baseFolder))
return "Tried to read outside of base folder";
else
return file.read(resolvedPath);
反模式 - 尝试清理文件名
人们可能很想这样做:
let baseFolder = "/var/www/api/documents/";
let path = baseFolder + request.params.filename.replace("../", "");
...
然而,这种方法不应该使用。处理路径的关键在于始终关注规范化后的路径。
只要规范化路径没有违反任何规则,路径最终是如何构建的其实并不重要。像这样尝试清理路径非常容易出错,而且几乎从不安全。
限制访问
在之前的示例中,我们使用了读取“/etc/shadow”文件的例子,这是 Linux 上存储密码哈希的文件。但实际上,应用程序没有理由能够读取该文件,或根目录之外的其他文件。
如果你使用容器,你可能已经规避了许多风险。采取措施加固容器(例如不要以 root 身份运行)至关重要。强烈建议剥离 Web 进程的所有权限,并将其对文件系统的读取权限限制在严格必要的文件范围内。
示例
现在,我们将分享一些不同语言的示例,以帮助更好地演示这些操作的实际效果。
C# - 不安全
如果不解析完整路径,或不确保仅使用路径中的文件名部分,代码将容易受到路径遍历攻击。
var baseFolder = "/var/www/app/documents/";
var fileName = "../../../../../etc/passwd";
// 不安全:读取 /etc/passwd
var fileContents = File.ReadAllText(Path.Combine(baseFolder, fileName));
C# - 安全 - 规范化
在此示例中,我们通过解析完整(绝对)路径并确保解析后的文件路径位于基础文件夹内,从而防范路径遍历攻击。
var baseFolder = "/var/www/app/documents/";
var fileName = "../../../../../etc/passwd";
var canonicalPath = Path.GetFullPath(Path.Combine(baseFolder, fileName));
// 安全:拒绝任何读取指定基础目录之外文件的尝试。
if(!canonicalPath.StartsWith(baseFolder))
return "Trying to read file outside of base folder";
var fileContents = File.ReadAllText(canonicalPath);
C# - 安全 - 文件名
在此示例中,我们仅提取路径中的文件名部分,从而防范路径遍历攻击,确保无法跳出指定的文件夹。
var baseFolder = "/var/www/app/documents/";
// 仅在不允许导航至其他子文件夹时使用此方法
var fileName = Path.GetFileName("../../../../../etc/passwd");
// 安全:读取 /var/www/app/documents/passwd
var fileContents = File.ReadAllText(Path.Combine(baseFolder, fileName));
Java - 不安全
如果不解析完整路径,或不确保仅使用路径中的文件名部分,代码将容易受到路径遍历攻击。
String baseFolder = "/var/www/app/documents/";
String fileName = "../../../../../etc/passwd";
// 不安全:读取 /etc/passwd
Path filePath = Paths.get(baseFolder + fileName);
List<String> lines = Files.readAllLines(filePath);
Java - 安全 - 规范化路径
在此示例中,我们通过解析完整(绝对)路径并确保解析后的文件路径位于基础文件夹内,从而防范路径遍历攻击。
String baseFolder = "/var/www/app/documents/";
String fileName = "../../../../../etc/passwd";
// 不安全:读取 /etc/passwd
Path normalizedPath = Paths.get(baseFolder + fileName).normalize();
if(!normalizedPath.toString().startsWith(baseFolder))
{
return "尝试读取根目录之外的路径";
}
else
{
List<String> lines = Files.readAllLines(normalizedPath);
}
Java - 安全 - 文件名
在此示例中,我们仅提取路径中的文件名部分,从而防范路径遍历攻击,确保无法跳出指定的文件夹。
String baseFolder = "/var/www/app/documents/";
// 仅在不允许导航至其他子文件夹时使用此方法
String fileName = Paths.get("../../../../../etc/passwd").getFileName().toString();
// 安全:读取 /var/www/app/documents/passwd
Path filePath = Paths.get(baseFolder + fileName);
List<String> lines = Files.readAllLines(filePath);
Javascript - 不安全
如果不解析完整路径,或不确保仅使用路径中的文件名部分,代码将容易受到路径遍历攻击。
const fs = require('fs');
const baseFolder = "/var/www/app/documents/";
const fileName = "../../../../../etc/passwd";
// 不安全:读取 /etc/passwd
const data = fs.readFileSync(baseFolder + fileName, 'utf8');
Javascript - 安全 - 规范化路径
在此示例中,我们通过解析完整(绝对)路径并确保解析后的文件路径位于基础文件夹内,从而防范路径遍历攻击。
const fs = require("fs");
const path = require("path");
const baseFolder = "/var/www/app/documents/";
const fileName = "../../../../../etc/passwd";
const normalizedPath = path.normalize(path.join(baseFolder, fileName));
// 安全:读取 /var/www/app/documents/passwd
const data = fs.readFileSync(normalizedPath, 'utf8');
Javascript - 安全 - 文件名
在此示例中,我们仅提取路径中的文件名部分,从而防范路径遍历攻击,确保无法跳出指定的文件夹。
const fs = require("fs");
const path = require("path");
const baseFolder = "/var/www/app/documents/";
const fileName = path.basename("../../../../../etc/passwd");
// 安全:读取 /var/www/app/documents/passwd
const data = fs.readFileSync(path.join(baseFolder, fileName), 'utf8');
Python - 不安全
如果不解析完整路径,或不确保仅使用路径中的文件名部分,代码将容易受到路径遍历攻击。
baseFolder = "/var/www/app/documents/"
fileName = "../../../../../etc/passwd"
# 不安全:读取 /etc/passwd
fileContents = open(baseFolder + fileName).read()
Python - 安全 - 规范化路径
在此示例中,我们通过解析完整(绝对)路径并确保解析后的文件路径位于基础文件夹内,从而防范路径遍历攻击。
import os.path
baseFolder = "/var/www/app/documents/"
fileName = "../../../../../etc/passwd"
normalizedPath = os.path.normpath(baseFolder + fileName)
# 安全:拒绝任何读取指定基础文件夹之外文件的尝试
if not normalizedPath.startswith(baseFolder):
return "尝试读取基础文件夹之外的内容"
# 安全:读取 /var/www/app/documents/passwd
fileContents = open(normalizedPath).read()
Python - 安全 - 文件名
在此示例中,我们仅提取路径中的文件名部分,从而防范路径遍历攻击,确保无法跳出指定的文件夹。
import os.path
baseFolder = "/var/www/app/documents/"
fileName = os.path.basename("../../../../../etc/passwd")
# 安全:读取 /var/www/app/documents/passwd
fileContents = open(os.path.join(baseFolder, fileName)).read()
实用安全编码
探索安全编码指南,了解并缓解 OWASP Top 10 等软件漏洞,并通过引导式训练任务在真实的应用程序模拟中进行实践。