Files
UNSWebServerCore_LinuxSO/PathTraversal.cpp
T
2026-06-30 17:49:51 +08:00

176 lines
5.7 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "PathTraversal.h"
#include <vector>
#include <algorithm>
bool PathTraversal::InlineHasTraversalPattern(std::string_view path)
{
if (path == "..")
return true;
if (path.rfind("../", 0) == 0)
return true;
if (path.find("/../") != std::string::npos)
return true;
if (path.size() >= 3 && path.compare(path.size() - 3, 3, "/..") == 0)
return true;
return false;
}
std::string PathTraversal::UrlDecode(std::string_view src)
{
std::string dst;
dst.reserve(src.length()); // 预分配内存,优化性能
for (size_t i = 0; i < src.length(); ++i)
{
// 确保 % 后面至少还有两个字符
if ((src[i] == '%') && ((i + 2) < src.length()))
{
// 转换为 unsigned char 规避 std::isxdigit 在处理非 ASCII(如UTF-8) 时的未定义行为
unsigned char hi = static_cast<unsigned char>(src[i + 1]);
unsigned char lo = static_cast<unsigned char>(src[i + 2]);
if (std::isxdigit(hi) && std::isxdigit(lo))
{
auto hexToChar = [] (unsigned char c) -> int
{
if ((c >= '0') && (c <= '9'))
return c - '0';
if ((c >= 'a') && (c <= 'f'))
return c - 'a' + 10;
if ((c >= 'A') && (c <= 'F'))
return c - 'A' + 10;
return 0;
};
dst += static_cast<char>((hexToChar(hi) << 4) | hexToChar(lo));
i += 2;
continue;
}
}
dst += src[i];
}
return dst;
}
bool PathTraversal::HasPathTraversalPattern(std::string_view raw_url_path)
{
if (raw_url_path.empty())
return false;
// 1. 进行 URL 解码,让隐藏的 %2f, %2e 现出原形
std::string decoded = UrlDecode(raw_url_path);
// 2. 统一将 Windows 风格的反斜杠 `\` 替换为正斜杠 `/`,防止利用反斜杠绕过
std::replace(decoded.begin(), decoded.end(), '\\', '/');
// 3. 特征级严格匹配:
// 路径穿透的本质是形成一个独立的 ".." 路径层级。
// 它只可能以四种形态存在:".."、"../开头的路径"、"/../中间路径"、以及"/.."结尾的路径。
if (decoded == "..")
return true;
if (decoded.rfind("../", 0) == 0) // 检查是否以 "../" 开头
return true;
if (decoded.find("/../") != std::string::npos) // 检查是否包含 "/../"
return true;
if (decoded.size() >= 3 && decoded.compare(decoded.size() - 3, 3, "/..") == 0) // 检查是否以 "/.." 结尾
return true;
return false;
}
bool PathTraversal::IsSafePath(const std::filesystem::path& base_path, const std::filesystem::path& user_path)
{
namespace fs = std::filesystem;
try
{
// 1. 规范化沙盒基准路径
fs::path canonical_base = fs::weakly_canonical(base_path);
// 2. 拼接并规范化目标路径
// 注意:若 user_path 为绝对路径(如 /etc/passwd),operator/ 会直接覆盖前面的路径,
// weakly_canonical 依然能正确将其解析为最终的物理绝对路径。
fs::path canonical_target = fs::weakly_canonical(canonical_base / user_path);
// 3. 计算逻辑相对关系
auto rel = canonical_target.lexically_relative(canonical_base);
// 4. 边界一票否决制:
// - 如果 rel 为空,说明两者不在同一根目录下(例如 Windows 跨盘符 C:\ 到 D:\)
// - 如果相对路径的第一个组件是 "..", 说明逻辑路径已经逃逸出根目录
if (rel.empty() || *rel.begin() == "..")
return false;
return true;
}
catch (...)
{
// 捕获任何潜在的系统路径解析异常(如超长路径、非法字符等)
return false;
}
}
PathTraversal::UrlSafetyStatus PathTraversal::AnalyzeUrlTraversal(std::string_view raw_url_path)
{
// 1. 检查原始明文中是否存在穿透特征(统一斜杠处理)
std::string raw_normalized_slash(raw_url_path);
std::replace(raw_normalized_slash.begin(), raw_normalized_slash.end(), '\\', '/');
bool raw_has_traversal = InlineHasTraversalPattern(raw_normalized_slash);
// 2. 进行 URL 解码,让隐藏的 %2f, %2e, %5c 现出原形
std::string decoded_path = UrlDecode(raw_url_path);
std::replace(decoded_path.begin(), decoded_path.end(), '\\', '/');
bool decoded_has_traversal = InlineHasTraversalPattern(decoded_path);
// 3. 对比前后差异
if (decoded_has_traversal)
{
if (!raw_has_traversal) // 【核心逻辑】明文看起来很安全,解码后突然蹦出穿透特征 -> 判定为转义绕过攻击
return UrlSafetyStatus::EvasiveTraversal;
// 明文和密文都有,属于客户端手荡或老旧 SDK 拼接未规范化
return UrlSafetyStatus::LiteralTraversal;
}
return UrlSafetyStatus::Safe;
}
std::string PathTraversal::NormalizeUrlPath(std::string_view decoded_url_path)
{
if (decoded_url_path.empty())
return "/";
std::vector<std::string_view> segments;
size_t start = 0;
// 逻辑切分路径组件
while (start < decoded_url_path.length())
{
size_t end = decoded_url_path.find_first_of("/\\", start);
std::string_view segment = decoded_url_path.substr(start, end - start);
if (!segment.empty() && segment != ".")
{
if (segment == "..")
{
// 遇到 .. 则弹栈,实现路径向上坍缩
if (!segments.empty())
segments.pop_back();
}
else
segments.push_back(segment);
}
if (end == std::string_view::npos)
break;
start = end + 1;
}
// 重新拼接规范化后的标准 URL 路径
std::string result;
for (const auto& seg : segments)
{
result += "/";
result.append(seg);
}
return result.empty() ? "/" : result;
}
std::string PathTraversal::ToString(UrlSafetyStatus s)
{
switch(s)
{
case UrlSafetyStatus::Safe:
return "Safe";
case UrlSafetyStatus::LiteralTraversal:
return "LiteralTraversal";
case UrlSafetyStatus::EvasiveTraversal:
return "EvasiveTraversal";
default:
return "Unknown??";
}
return "Unknown??";
}