-
Notifications
You must be signed in to change notification settings - Fork 4.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Sanitize headers nominated by the Connection header #8862
Changes from all commits
29b01f1
06726ef
2d3bcca
00f8e5e
8484cd5
571e2f8
a35b697
1ab0d61
6174482
26bfd63
e8cd453
beb9efd
3f095c6
a20cbe6
7f367f9
a09025e
7667971
3a5c2fa
c111350
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -24,6 +24,7 @@ | |
|
||
#include "absl/strings/match.h" | ||
#include "absl/strings/str_cat.h" | ||
#include "absl/strings/str_split.h" | ||
|
||
namespace Envoy { | ||
namespace Http { | ||
|
@@ -387,6 +388,129 @@ Utility::getLastAddressFromXFF(const Http::HeaderMap& request_headers, uint32_t | |
} | ||
} | ||
|
||
bool Utility::sanitizeConnectionHeader(Http::HeaderMap& headers) { | ||
static const size_t MAX_ALLOWED_NOMINATED_HEADERS = 10; | ||
static const size_t MAX_ALLOWED_TE_VALUE_SIZE = 256; | ||
|
||
// Remove any headers nominated by the Connection header. The TE header | ||
// is sanitized and removed only if it's empty after removing unsupported values | ||
// See https://github.com/envoyproxy/envoy/issues/8623 | ||
const auto& cv = Http::Headers::get().ConnectionValues; | ||
const auto& connection_header_value = headers.Connection()->value(); | ||
|
||
StringUtil::CaseUnorderedSet headers_to_remove{}; | ||
std::vector<absl::string_view> connection_header_tokens = | ||
StringUtil::splitToken(connection_header_value.getStringView(), ",", false); | ||
|
||
// If we have 10 or more nominated headers, fail this request | ||
if (connection_header_tokens.size() >= MAX_ALLOWED_NOMINATED_HEADERS) { | ||
ENVOY_LOG_MISC(trace, "Too many nominated headers in request"); | ||
return false; | ||
} | ||
|
||
// Split the connection header and evaluate each nominated header | ||
for (const auto& token : connection_header_tokens) { | ||
|
||
const auto token_sv = StringUtil::trim(token); | ||
|
||
// Build the LowerCaseString for header lookup | ||
const LowerCaseString lcs_header_to_remove{std::string(token_sv)}; | ||
|
||
// If the Connection token value is not a nominated header, ignore it here since | ||
// the connection header is removed elsewhere when the H1 request is upgraded to H2 | ||
if ((lcs_header_to_remove.get() == cv.Close) || | ||
(lcs_header_to_remove.get() == cv.Http2Settings) || | ||
(lcs_header_to_remove.get() == cv.KeepAlive) || | ||
(lcs_header_to_remove.get() == cv.Upgrade)) { | ||
continue; | ||
} | ||
|
||
// By default we will remove any nominated headers | ||
bool keep_header = false; | ||
|
||
// Determine whether the nominated header contains invalid values | ||
HeaderEntry* nominated_header = NULL; | ||
|
||
if (lcs_header_to_remove == Http::Headers::get().Connection) { | ||
// Remove the connection header from the nominated tokens if it's self nominated | ||
// The connection header itself is *not removed* | ||
ENVOY_LOG_MISC(trace, "Skipping self nominated header [{}]", token_sv); | ||
keep_header = true; | ||
headers_to_remove.emplace(token_sv); | ||
|
||
} else if ((lcs_header_to_remove == Http::Headers::get().ForwardedFor) || | ||
(lcs_header_to_remove == Http::Headers::get().ForwardedHost) || | ||
(lcs_header_to_remove == Http::Headers::get().ForwardedProto) || | ||
!token_sv.find(':')) { | ||
|
||
// An attacker could nominate an X-Forwarded* header, and its removal may mask | ||
// the origin of the incoming request and potentially alter its handling. | ||
// Additionally, pseudo headers should never be nominated. In both cases, we | ||
// should fail the request. | ||
// See: https://nathandavison.com/blog/abusing-http-hop-by-hop-request-headers | ||
|
||
ENVOY_LOG_MISC(trace, "Invalid nomination of {} header", token_sv); | ||
return false; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we trace log this as well for consistency? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep. Added a trace log here |
||
} else { | ||
// Examine the value of all other nominated headers | ||
nominated_header = headers.get(lcs_header_to_remove); | ||
} | ||
|
||
if (nominated_header) { | ||
auto nominated_header_value_sv = nominated_header->value().getStringView(); | ||
|
||
const bool is_te_header = (lcs_header_to_remove == Http::Headers::get().TE); | ||
|
||
// reject the request if the TE header is too large | ||
if (is_te_header && (nominated_header_value_sv.size() >= MAX_ALLOWED_TE_VALUE_SIZE)) { | ||
ENVOY_LOG_MISC(trace, "TE header contains a value that exceeds the allowable length"); | ||
return false; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This may need a log message. Because form the log message printed by the caller it would be unclear why sanitizeConnectionHeader failed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Certainly |
||
} | ||
|
||
if (is_te_header) { | ||
for (const auto& header_value : | ||
StringUtil::splitToken(nominated_header_value_sv, ",", false)) { | ||
|
||
const absl::string_view header_sv = StringUtil::trim(header_value); | ||
|
||
// If trailers exist in the TE value tokens, keep the header, removing any other values | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. here I assume we don't want to use StringUtil::caseFindToken because it doesn't allow the limitation. |
||
// that may exist | ||
if (StringUtil::CaseInsensitiveCompare()(header_sv, | ||
Http::Headers::get().TEValues.Trailers)) { | ||
keep_header = true; | ||
break; | ||
} | ||
} | ||
|
||
if (keep_header) { | ||
nominated_header->value().setCopy(Http::Headers::get().TEValues.Trailers.data(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe setCopy implicitly clears There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will remove the |
||
Http::Headers::get().TEValues.Trailers.size()); | ||
} | ||
} | ||
} | ||
|
||
if (!keep_header) { | ||
ENVOY_LOG_MISC(trace, "Removing nominated header [{}]", token_sv); | ||
headers.remove(lcs_header_to_remove); | ||
headers_to_remove.emplace(token_sv); | ||
} | ||
} | ||
|
||
// Lastly remove extra nominated headers from the Connection header | ||
if (!headers_to_remove.empty()) { | ||
const std::string new_value = StringUtil::removeTokens(connection_header_value.getStringView(), | ||
",", headers_to_remove, ","); | ||
|
||
if (new_value.empty()) { | ||
headers.removeConnection(); | ||
} else { | ||
headers.Connection()->value(new_value); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. does removeTokens remove empty fields? |
||
} | ||
} | ||
|
||
return true; | ||
} | ||
|
||
const std::string& Utility::getProtocolString(const Protocol protocol) { | ||
switch (protocol) { | ||
case Protocol::Http10: | ||
|
@@ -568,8 +692,8 @@ std::string Utility::PercentEncoding::encode(absl::string_view value) { | |
// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#responses. | ||
// | ||
// We do checking for each char in the string. If the current char is included in the defined | ||
// escaping characters, we jump to "the slow path" (append the char [encoded or not encoded] to | ||
// the returned string one by one) started from the current index. | ||
// escaping characters, we jump to "the slow path" (append the char [encoded or not encoded] | ||
// to the returned string one by one) started from the current index. | ||
if (ch < ' ' || ch >= '~' || ch == '%') { | ||
return PercentEncoding::encode(value, i); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you add release notes at docs/root/intro/version_history.rst including the flag to flip to revert to prior behavior?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure thing!