Skip to content

Commit a4a84dc

Browse files
bsergeanclaude
andcommitted
Fix: path traversal in HttpServer default callback (CVE candidate)
The default HttpServer callback built the filesystem path as "." + uri without sanitizing .. segments, allowing a remote unauthenticated client to read files outside the document root with a request like GET /../secret.txt. Add sanitizeUri() which resolves . and .. lexically, clamping any attempt to escape above the virtual root so the path stays within the document root directory. Reported by Yeongtaek Yoo (GitHub: @yt010108). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 500eee4 commit a4a84dc

1 file changed

Lines changed: 40 additions & 5 deletions

File tree

ixwebsocket/IXHttpServer.cpp

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,54 @@ namespace
4343
return std::make_pair(res.first, std::string(vec.begin(), vec.end()));
4444
}
4545

46-
std::string response_head_file(const std::string& file_name){
46+
// Normalize a URI by resolving . and .. segments so that the resulting
47+
// path never escapes the document root. Attempts to traverse above /
48+
// (e.g. GET /../secret) are silently clamped rather than rejected so that
49+
// the caller receives a predictable 404 instead of a server error.
50+
std::string sanitizeUri(const std::string& uri)
51+
{
52+
std::vector<std::string> parts;
53+
std::string token;
54+
std::istringstream stream(uri);
55+
56+
while (std::getline(stream, token, '/'))
57+
{
58+
if (token == "..")
59+
{
60+
if (!parts.empty()) parts.pop_back();
61+
// else: silently ignore attempts to escape above root
62+
}
63+
else if (!token.empty() && token != ".")
64+
{
65+
parts.push_back(token);
66+
}
67+
}
68+
69+
std::string sanitized;
70+
for (const auto& part : parts)
71+
{
72+
sanitized += "/" + part;
73+
}
4774

48-
if (std::string::npos != file_name.find(".html") || std::string::npos != file_name.find(".htm"))
75+
return sanitized.empty() ? "/" : sanitized;
76+
}
77+
78+
std::string response_head_file(const std::string& file_name)
79+
{
80+
if (std::string::npos != file_name.find(".html") ||
81+
std::string::npos != file_name.find(".htm"))
4982
return "text/html";
5083
else if (std::string::npos != file_name.find(".css"))
5184
return "text/css";
52-
else if (std::string::npos != file_name.find(".js") || std::string::npos != file_name.find(".mjs"))
85+
else if (std::string::npos != file_name.find(".js") ||
86+
std::string::npos != file_name.find(".mjs"))
5387
return "application/x-javascript";
5488
else if (std::string::npos != file_name.find(".ico"))
5589
return "image/x-icon";
5690
else if (std::string::npos != file_name.find(".png"))
5791
return "image/png";
58-
else if (std::string::npos != file_name.find(".jpg") || std::string::npos != file_name.find(".jpeg"))
92+
else if (std::string::npos != file_name.find(".jpg") ||
93+
std::string::npos != file_name.find(".jpeg"))
5994
return "image/jpeg";
6095
else if (std::string::npos != file_name.find(".gif"))
6196
return "image/gif";
@@ -125,7 +160,7 @@ namespace ix
125160
[this](HttpRequestPtr request,
126161
std::shared_ptr<ConnectionState> connectionState) -> HttpResponsePtr
127162
{
128-
std::string uri(request->uri);
163+
std::string uri(sanitizeUri(request->uri));
129164
if (uri.empty() || uri == "/")
130165
{
131166
uri = "/index.html";

0 commit comments

Comments
 (0)