176 lines
5.7 KiB
C++
176 lines
5.7 KiB
C++
#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??";
|
||
}
|