security hardening - #2538
Conversation
Alexandru2984
commented
Jul 2, 2026
- Fix path traversal on Windows in static file router
- Reject ambiguous request framing to prevent HTTP request smuggling
- Strip CR/LF from response headers, redirects and cookies
- Validate the comparison operator in Criteria's JSON constructor
The traversal-depth check only split the request path on '/', so a target using backslash separators (e.g. "..%5c..%5c", url-decoded to "..\..\") produced a single path segment that was never recognized as "..". On Windows, where the filesystem treats '\' as a directory separator, this allowed escaping the document root and reading files with a whitelisted extension. Normalize backslashes to forward slashes in a copy of the path used solely for the traversal check; the path used to locate the file is left unchanged, so legitimate filenames are unaffected.
The request parser accepted several forms of ambiguous message framing
that a front-end proxy might interpret differently, enabling request
smuggling:
- A request carrying both Content-Length and Transfer-Encoding was
handled by reading Content-Length bytes and ignoring the chunked
encoding (a CL.TE desync). Such requests are now rejected with 400.
- Multiple Content-Length headers with differing values were collapsed
to the first value with no error. They are now detected in addHeader
and rejected with 400.
- Content-Length values were parsed with std::stoull, which silently
accepts a leading sign ("-1" wrapping to a huge size_t) and trailing
garbage ("5abc"). The value is now required to be digits only.
Also harden chunked parsing: the chunk size was read with signed strtol
and added to the accumulated length before the size check, which could
wrap past the limit. It now uses strtoull, validates the field, and
compares against the remaining allowance without overflowing.
Header names and values set via HttpResponse::addHeader, the redirect target, and the Set-Cookie fields (key, value, domain, path) were written into the response verbatim. When any of these are derived from untrusted input, an embedded "\r\n" allowed HTTP response splitting / header injection. Strip CR and LF characters from these fields at the point they are placed into the response, so injected line breaks can no longer forge additional headers or a second response.
Criteria(const Json::Value&) is used by the generated RESTful controllers to build a WHERE clause from the client-supplied "filter" query. The value is bound as a parameter, but the comparison operator (json[1]) was concatenated into the SQL string verbatim, so a filter such as ["id", "= 1 OR 1=1 --", 0] injected arbitrary SQL. This holds even for the default masquerading controllers, which whitelist the column name but never checked the operator. Restrict the operator to a fixed allowlist (=, !=, <>, >, >=, <, <=, like, not like, ilike, not ilike), normalized for spacing and case, and throw on anything else. The field name still cannot be parameterized, so document that it must be trusted / whitelisted by the caller.
| auto sanitizedValue = sanitizeHeaderField(value); | ||
| headers_[sanitizeHeaderField(std::move(field))] = | ||
| std::move(sanitizedValue); |
There was a problem hiding this comment.
For the field parameter – This is almost always a hard‑coded string (either from framework internals or from the developer). It rarely, if ever, comes from untrusted input. So we could skip sanitization entirely for the field name, or at least treat it separately to reduce overhead.
For the value parameter – Instead of unconditionally calling erase + remove_if on every invocation, we can first perform a quick scan using find_first_of("\r\n"). In the vast majority of cases (legitimate values contain no CR or LF), this returns npos and we can use the original string immediately, avoiding any memory writes and moves. Only when the scan detects a problematic character do we enter the slower cleaning path.
if (value.find_first_of("\r\n")!=std::string::npos)
{
headers_[std::move(field)] = sanitizeHeaderField(value);
}
else
{
headers_[std::move(field)] = value;
}|
@Alexandru2984 Thanks for this important security fix. |