mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-21 11:39:48 +00:00
Add filename sanitization function and tests to prevent path traversal vulnerabilities
This commit is contained in:
1 parent
4d7c9a788d
commit
83e98a28dd
3 files changed
+59
-4
No files matched your search
@@ -541,16 +541,16 @@ svr.Post("/multipart", [&](const Request& req, Response& res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IMPORTANT: file.filename is an untrusted value from the client.
|
// IMPORTANT: file.filename is an untrusted value from the client.
|
||||||
// Always extract only the basename to prevent path traversal attacks.
|
// Always sanitize to prevent path traversal attacks.
|
||||||
auto safe_name = std::filesystem::path(file.filename).filename();
|
auto safe_name = httplib::sanitize_filename(file.filename);
|
||||||
if (safe_name.empty() || safe_name == "." || safe_name == "..") {
|
if (safe_name.empty()) {
|
||||||
res.status = StatusCode::BadRequest_400;
|
res.status = StatusCode::BadRequest_400;
|
||||||
res.set_content("Invalid filename", "text/plain");
|
res.set_content("Invalid filename", "text/plain");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save to disk
|
// Save to disk
|
||||||
std::ofstream ofs(upload_dir / safe_name, std::ios::binary);
|
std::ofstream ofs(upload_dir + "/" + safe_name, std::ios::binary);
|
||||||
ofs << file.content;
|
ofs << file.content;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,6 +586,16 @@ svr.Post("/multipart", [&](const Request& req, Response& res) {
|
|||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Filename Sanitization
|
||||||
|
|
||||||
|
`file.filename` in multipart uploads is an untrusted value from the client. Always sanitize before using it in file paths:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
auto safe = httplib::sanitize_filename(file.filename);
|
||||||
|
```
|
||||||
|
|
||||||
|
This function strips path separators (`/`, `\`), null bytes, leading/trailing whitespace, and rejects `.` and `..`. Returns an empty string if the filename is unsafe.
|
||||||
|
|
||||||
### Receive content with a content receiver
|
### Receive content with a content receiver
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
|
|||||||
@@ -2915,6 +2915,8 @@ std::string encode_query_component(const std::string &component,
|
|||||||
std::string decode_query_component(const std::string &component,
|
std::string decode_query_component(const std::string &component,
|
||||||
bool plus_as_space = true);
|
bool plus_as_space = true);
|
||||||
|
|
||||||
|
std::string sanitize_filename(const std::string &filename);
|
||||||
|
|
||||||
std::string append_query_params(const std::string &path, const Params ¶ms);
|
std::string append_query_params(const std::string &path, const Params ¶ms);
|
||||||
|
|
||||||
std::pair<std::string, std::string> make_range_header(const Ranges &ranges);
|
std::pair<std::string, std::string> make_range_header(const Ranges &ranges);
|
||||||
@@ -9395,6 +9397,30 @@ inline std::string decode_query_component(const std::string &component,
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline std::string sanitize_filename(const std::string &filename) {
|
||||||
|
// Extract basename: find the last path separator (/ or \)
|
||||||
|
auto pos = filename.find_last_of("/\\");
|
||||||
|
auto result =
|
||||||
|
(pos != std::string::npos) ? filename.substr(pos + 1) : filename;
|
||||||
|
|
||||||
|
// Strip null bytes
|
||||||
|
result.erase(std::remove(result.begin(), result.end(), '\0'), result.end());
|
||||||
|
|
||||||
|
// Trim whitespace
|
||||||
|
{
|
||||||
|
auto start = result.find_first_not_of(" \t");
|
||||||
|
auto end = result.find_last_not_of(" \t");
|
||||||
|
result = (start == std::string::npos)
|
||||||
|
? ""
|
||||||
|
: result.substr(start, end - start + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject . and ..
|
||||||
|
if (result == "." || result == "..") { return ""; }
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
inline std::string append_query_params(const std::string &path,
|
inline std::string append_query_params(const std::string &path,
|
||||||
const Params ¶ms) {
|
const Params ¶ms) {
|
||||||
std::string path_with_query = path;
|
std::string path_with_query = path;
|
||||||
|
|||||||
@@ -413,6 +413,25 @@ TEST(DecodePathTest, PercentCharacterNUL) {
|
|||||||
EXPECT_EQ(decode_path_component("x%00x"), expected);
|
EXPECT_EQ(decode_path_component("x%00x"), expected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(SanitizeFilenameTest, VariousPatterns) {
|
||||||
|
// Path traversal
|
||||||
|
EXPECT_EQ("passwd", httplib::sanitize_filename("../../../etc/passwd"));
|
||||||
|
EXPECT_EQ("passwd", httplib::sanitize_filename("..\\..\\etc\\passwd"));
|
||||||
|
EXPECT_EQ("file.txt", httplib::sanitize_filename("path/to\\..\\file.txt"));
|
||||||
|
// Normal and edge cases
|
||||||
|
EXPECT_EQ("photo.jpg", httplib::sanitize_filename("photo.jpg"));
|
||||||
|
EXPECT_EQ("filename.txt",
|
||||||
|
httplib::sanitize_filename("/path/to/filename.txt"));
|
||||||
|
EXPECT_EQ(".gitignore", httplib::sanitize_filename(".gitignore"));
|
||||||
|
EXPECT_EQ("", httplib::sanitize_filename(".."));
|
||||||
|
EXPECT_EQ("", httplib::sanitize_filename(""));
|
||||||
|
// Null bytes stripped
|
||||||
|
EXPECT_EQ("safe.txt",
|
||||||
|
httplib::sanitize_filename(std::string("safe\0.txt", 9)));
|
||||||
|
// Whitespace-only rejected
|
||||||
|
EXPECT_EQ("", httplib::sanitize_filename(" "));
|
||||||
|
}
|
||||||
|
|
||||||
TEST(EncodeQueryParamTest, ParseUnescapedChararactersTest) {
|
TEST(EncodeQueryParamTest, ParseUnescapedChararactersTest) {
|
||||||
string unescapedCharacters = "-_.!~*'()";
|
string unescapedCharacters = "-_.!~*'()";
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user