upload
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
#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??";
|
||||
}
|
||||
Reference in New Issue
Block a user