注入 - 路径遍历
路径遍历是另一种相当常见的注入漏洞。当构建 URI(无论是 URL、文件路径还是其他)时,如果不能正确确保完全解析的路径不会指向目标路径的根目录之外,就会发生这种情况。
需要指出的是,路径遍历实际上也可以被视为路径*注入*漏洞。
路径遍历漏洞的影响在很大程度上取决于发生遍历的环境,以及所做的整体加固。不过,在讨论这个问题之前,让我们先看一个关于这个漏洞的快速实用示例,看看我们在说什么:
简单分析
考虑在应用程序中设置一个端点来提供文档,如合同模板或招聘信息。这些文件都可以是静态文件,如 PDF。
在这种情况下,您可能需要这样一段代码来根据请求获取文件:
let baseFolder = "/var/www/api/documents/";
let path = baseFolder + request.params.filename;
return file.read(path);
为了演示漏洞是如何出现的,我们还必须知道应用程序的根目录在哪里,因此在本例中,假设应用程序的根目录在"/var/www/api/"。
我们知道应用程序需要一个 "文件名 "参数,下面我们来看几个输入的例子,以及结果是什么:
请注意我们是如何使用".../"遍历文件系统的。我们可以跳出通常存放 PDF 文件的 "documents "文件夹,进入包含 "shadow "文件的"/etc/"文件夹,在 Linux 系统中,"shadow "文件包含密码哈希值。可想而知,这并不理想。
查看 Urls 中的遍历
路径遍历的另一种变体可能发生在构建用于与 API 交互的 URL 时。假设我们有一个具有以下方法的应用程序接口:
另一个应用程序在试图获取订单信息时可能会调用 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。
规范化通常不在客户端进行(尽管可以),但网络服务器会将请求规范化为下面的格式。
在第二个示例的输入中,我们没有获取 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 上的密码哈希值文件。但实际上,应用程序没有理由读取该文件或其根目录以外的其他文件。
如果您使用容器,很可能已经降低了很多风险。采取措施加固容器(不要以根用户身份运行等)至关重要。强烈建议取消网络进程的所有权限,并将其在文件系统上的读取权限限制在其严格需要的文件范围内。
实例
现在,我们将分享几个不同语言的示例,以帮助更好地演示它们的操作。
C# - 不安全
如果不解析完整路径,或确保只使用路径中的文件名部分,就会使代码容易受到路径遍历的攻击。
var baseFolder = "/var/www/app/documents/";
var fileName = ".../.../.../.../etc/passwd";
// INSECURE:读取 /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));
// SECURE:
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";
// INSECURE: Reads /etc/passwd
Path filePath = Paths.get(baseFolder + fileName);
List<String> lines = Files.readAllLines(filePath);
Java - 安全 - Canonical
在本例中,我们通过解析完整(绝对)路径来防止路径遍历,并确保解析的文件路径位于基本文件夹内。
String baseFolder = "/var/www/app/documents/";
String fileName = "../../../../../etc/passwd";
// INSECURE: Reads /etc/passwd
Path normalizedPath = Paths.get(baseFolder + fileName).normalize();
if(!normalizedPath.toString().startsWith(baseFolder))
{
return "Trying to read path outside of root";
}
else
{
List<String> lines = Files.readAllLines(normalizedPath);
}
Java - 安全 - 文件名
在这个示例中,我们只使用路径中的文件名部分来防止路径遍历,确保无法遍历指定的文件夹。
String baseFolder = "/var/www/app/documents/";
// Only use this if you don't allow navigating into other subfolders
String fileName = Paths.get("../../../../../etc/passwd").getFileName().toString();
// SECURE: Reads /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";
// INSECURE:读取 /etc/passwd
const data = fs.readFileSync(baseFolder + fileName, 'utf8');
Javascript - 安全 - Canonical
在本例中,我们通过解析完整(绝对)路径来防止路径遍历,并确保解析的文件路径位于基本文件夹内。
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));
// SECURE:读取 /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");
// SECURE:读取 /var/www/app/documents/passwd
const data = fs.readFileSync(path.join(baseFolder, fileName), 'utf8');
Python - 不安全
如果不解析完整路径,或确保只使用路径中的文件名部分,就会使代码容易受到路径遍历的攻击。
baseFolder = "/var/www/app/documents/"
fileName = ".../.../.../.../.../etc/passwd "
# INSECURE:读取 /etc/passwd
fileContents = open(baseFolder + fileName).read()
Python - 安全 - Canonical
在本例中,我们通过解析完整(绝对)路径来防止路径遍历,并确保解析的文件路径位于基本文件夹内。
import os.path
baseFolder = "/var/www/app/documents/"
fileName = "../.../.../.../.../etc/passwd "
normalizedPath = os.path.normpath(baseFolder + fileName)
# 安全:拒绝任何读取指定基本文件夹之外文件的尝试
if not normalizedPath.startswith(baseFolder):
return "Trying to read out of base folder"
# 安全:读取 /var/www/app/documents/passwd
fileContents = open(normalizedPath).read()
Python - 安全 - 文件名
在这个示例中,我们只使用路径中的文件名部分来防止路径遍历,确保无法遍历指定的文件夹。