mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-01 05:02:29 +07:00
Add filename sanitization function and tests to prevent path traversal vulnerabilities
This commit is contained in:
@@ -541,16 +541,16 @@ svr.Post("/multipart", [&](const Request& req, Response& res) {
|
||||
}
|
||||
|
||||
// IMPORTANT: file.filename is an untrusted value from the client.
|
||||
// Always extract only the basename to prevent path traversal attacks.
|
||||
auto safe_name = std::filesystem::path(file.filename).filename();
|
||||
if (safe_name.empty() || safe_name == "." || safe_name == "..") {
|
||||
// Always sanitize to prevent path traversal attacks.
|
||||
auto safe_name = httplib::sanitize_filename(file.filename);
|
||||
if (safe_name.empty()) {
|
||||
res.status = StatusCode::BadRequest_400;
|
||||
res.set_content("Invalid filename", "text/plain");
|
||||
return;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
```cpp
|
||||
|
||||
@@ -2915,6 +2915,8 @@ std::string encode_query_component(const std::string &component,
|
||||
std::string decode_query_component(const std::string &component,
|
||||
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::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;
|
||||
}
|
||||
|
||||
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,
|
||||
const Params ¶ms) {
|
||||
std::string path_with_query = path;
|
||||
|
||||
@@ -413,6 +413,25 @@ TEST(DecodePathTest, PercentCharacterNUL) {
|
||||
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) {
|
||||
string unescapedCharacters = "-_.!~*'()";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user