准则

注入 - 路径遍历

路径遍历是另一种相当常见的注入漏洞。当构建 URI(无论是 URL、文件路径还是其他)时,如果不能正确确保完全解析的路径不会指向目标路径的根目录之外,就会发生这种情况。 

需要指出的是,路径遍历实际上也可以被视为路径*注入*漏洞。 

路径遍历漏洞的影响在很大程度上取决于发生遍历的环境,以及所做的整体加固。不过,在讨论这个问题之前,让我们先看一个关于这个漏洞的快速实用示例,看看我们在说什么:                                                                                                        

简单分析

考虑在应用程序中设置一个端点来提供文档,如合同模板或招聘信息。这些文件都可以是静态文件,如 PDF。 

在这种情况下,您可能需要这样一段代码来根据请求获取文件:

let baseFolder = "/var/www/api/documents/";
let path = baseFolder + request.params.filename;

return file.read(path);

为了演示漏洞是如何出现的,我们还必须知道应用程序的根目录在哪里,因此在本例中,假设应用程序的根目录在"/var/www/api/"。 

我们知道应用程序需要一个 "文件名 "参数,下面我们来看几个输入的例子,以及结果是什么:

文件名 未解决的路径 已解决的路径
隐私.pdf /var/www/api/documents/Privacy.pdf /var/www/api/documents/Privacy.pdf
./config/prod.config /var/www/api/documents/../config/prod.config /var/www/api/config/prod.config
./././././././etc/shadow /var/www/api/documents/../../../../etc/shadow /etc/shadow

请注意我们是如何使用".../"遍历文件系统的。我们可以跳出通常存放 PDF 文件的 "documents "文件夹,进入包含 "shadow "文件的"/etc/"文件夹,在 Linux 系统中,"shadow "文件包含密码哈希值。可想而知,这并不理想。 

查看 Urls 中的遍历

路径遍历的另一种变体可能发生在构建用于与 API 交互的 URL 时。假设我们有一个具有以下方法的应用程序接口:

URL 模式 说明
/api/v1/order/get/{id} 获取指定 ID 订单的详细信息
/api/v1/order/delete/{id} 删除具有特定 ID 的订单

另一个应用程序在试图获取订单信息时可能会调用 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 号 实际调用的 URL
1 /api/v1/order/get/1
1/.../.../delete/1 /api/v1/order/delete/1

在第二个示例的输入中,我们没有获取 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 - 安全 - 文件名

在这个示例中,我们只使用路径中的文件名部分来防止路径遍历,确保无法遍历指定的文件夹。 

import os.path

baseFolder = "/var/www/app/documents/"
fileName = os.path.basename("../../../../etc/passwd")

# SECURE:读取 /var/www/app/documents/passwd
fileContents = open(os.path.join(baseFolder, fileName)).read()