Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Added the ``%REQUEST_HEADER_SHA256(X?Y)%`` and ``%REQ_SHA256(X?Y)%`` substitution
formatters to compute the SHA-256 digest of a request header without exposing its value.
15 changes: 15 additions & 0 deletions docs/root/configuration/advanced/substitution_formatter.rst
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,21 @@ Current supported substitution commands include:
TCP/UDP
Not implemented. It will appear as ``"-"`` in the access logs.

.. _config_access_log_format_req_sha256:

``%REQUEST_HEADER_SHA256(X?Y)%`` / ``%REQ_SHA256(X?Y)%``
HTTP
The lowercase hexadecimal SHA-256 digest of an HTTP request header. ``X`` is the main request
header and ``Y`` is an optional alternative. If ``X`` is absent or empty, the value of ``Y`` is
used instead. If neither header has a nonempty value, ``"-"`` appears in the access logs.

This formatter can be used to correlate requests or construct consistent-hashing keys without
copying a sensitive header value into another request header or log field. A SHA-256 digest is
not keyed, so values with low entropy may remain vulnerable to guessing.

TCP/UDP
Not implemented. It will appear as ``"-"`` in the access logs.

``%RESPONSE_HEADER(X?Y):Z%`` / ``%RESP(X?Y):Z%``
HTTP
Same as ``%REQUEST_HEADER(X?Y):Z%`` but taken from HTTP response headers.
Expand Down
3 changes: 3 additions & 0 deletions source/common/formatter/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,11 @@ envoy_cc_library(
"//envoy/runtime:runtime_interface",
"//envoy/stream_info:stream_info_interface",
"//envoy/upstream:upstream_interface",
"//source/common/buffer:buffer_lib",
"//source/common/common:assert_lib",
"//source/common/common:hex_lib",
"//source/common/common:utility_lib",
"//source/common/crypto:utility_lib",
"//source/common/formatter:substitution_format_utility_lib",
"//source/common/grpc:common_lib",
"//source/common/http:utility_lib",
Expand Down
55 changes: 55 additions & 0 deletions source/common/formatter/http_specific_formatter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@
#include <string>
#include <utility>

#include "source/common/buffer/buffer_impl.h"
#include "source/common/common/assert.h"
#include "source/common/common/empty_string.h"
#include "source/common/common/fmt.h"
#include "source/common/common/hex.h"
#include "source/common/common/thread.h"
#include "source/common/common/utility.h"
#include "source/common/config/metadata.h"
#include "source/common/crypto/utility.h"
#include "source/common/formatter/coalesce_formatter.h"
#include "source/common/grpc/common.h"
#include "source/common/grpc/status.h"
Expand Down Expand Up @@ -166,6 +169,40 @@ void RequestHeaderFormatter::formatValueTo(ValueSink& sink, const Context& conte
HeaderFormatter::formatValueTo(sink, context.requestHeaders());
}

RequestHeaderSha256Formatter::RequestHeaderSha256Formatter(absl::string_view main_header,
absl::string_view alternative_header)
: main_header_(main_header), alternative_header_(alternative_header) {}

std::optional<std::string>
RequestHeaderSha256Formatter::format(const Context& context, const StreamInfo::StreamInfo&) const {
const auto headers = context.requestHeaders();
if (!headers.has_value()) {
return std::nullopt;
}

const auto find_non_empty_header = [&headers](const Http::LowerCaseString& name) {
const auto values = headers->get(name);
return values.empty() || values[0]->value().empty() ? nullptr : values[0];
};

const Http::HeaderEntry* header = find_non_empty_header(main_header_);
if (header == nullptr && !alternative_header_.get().empty()) {
header = find_non_empty_header(alternative_header_);
}
if (header == nullptr) {
return std::nullopt;
}

return Hex::encode(Common::Crypto::UtilitySingleton::get().getSha256Digest(
Buffer::OwnedImpl(header->value().getStringView())));
}

Protobuf::Value
RequestHeaderSha256Formatter::formatValue(const Context& context,
const StreamInfo::StreamInfo& stream_info) const {
return ValueUtil::optionalStringValue(format(context, stream_info));
}

ResponseTrailerFormatter::ResponseTrailerFormatter(absl::string_view main_header,
absl::string_view alternative_header,
std::optional<size_t> max_length)
Expand Down Expand Up @@ -555,6 +592,24 @@ BuiltInHttpCommandParser::getKnownFormatters() {
return std::make_unique<RequestHeaderFormatter>(result.value().first,
result.value().second, max_length);
}}},
{"REQ_SHA256",
{CommandSyntaxChecker::PARAMS_REQUIRED,
[](absl::string_view format,
std::optional<size_t>) -> absl::StatusOr<FormatterProviderPtr> {
auto result = SubstitutionFormatUtils::parseSubcommandHeaders(format);
RETURN_IF_NOT_OK(result.status());
return std::make_unique<RequestHeaderSha256Formatter>(result.value().first,
result.value().second);
}}},
{"REQUEST_HEADER_SHA256",
{CommandSyntaxChecker::PARAMS_REQUIRED,
[](absl::string_view format,
std::optional<size_t>) -> absl::StatusOr<FormatterProviderPtr> {
auto result = SubstitutionFormatUtils::parseSubcommandHeaders(format);
RETURN_IF_NOT_OK(result.status());
return std::make_unique<RequestHeaderSha256Formatter>(result.value().first,
result.value().second);
}}},
{"RESP", // Same as RESPONSE_HEADER and used for backward compatibility.
{CommandSyntaxChecker::PARAMS_REQUIRED | CommandSyntaxChecker::LENGTH_ALLOWED,
[](absl::string_view format,
Expand Down
17 changes: 17 additions & 0 deletions source/common/formatter/http_specific_formatter.h
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,23 @@ class RequestHeaderFormatter : public FormatterProvider, HeaderFormatter {
const StreamInfo::StreamInfo& stream_info) const override;
};

/**
* FormatterProvider for the SHA-256 digest of a request header.
*/
class RequestHeaderSha256Formatter : public FormatterProvider {
public:
RequestHeaderSha256Formatter(absl::string_view main_header, absl::string_view alternative_header);

std::optional<std::string> format(const Context& context,
const StreamInfo::StreamInfo& stream_info) const override;
Protobuf::Value formatValue(const Context& context,
const StreamInfo::StreamInfo& stream_info) const override;

private:
Http::LowerCaseString main_header_;
Http::LowerCaseString alternative_header_;
};

/**
* FormatterProvider for response headers.
*/
Expand Down
52 changes: 52 additions & 0 deletions test/common/formatter/substitution_formatter_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3514,6 +3514,58 @@ TEST(SubstitutionFormatterTest, requestHeaderFormatter) {
}
}

TEST(SubstitutionFormatterTest, RequestHeaderSha256Formatter) {
StreamInfo::MockStreamInfo stream_info;
const std::string expected_digest =
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad";

Http::TestRequestHeaderMapImpl request_headers{{"authorization", "abc"},
{"x-request-id", "ignored"}};
Context formatter_context;
formatter_context.setRequestHeaders(request_headers);

RequestHeaderSha256Formatter formatter("AUTHORIZATION", "X-REQUEST-ID");
EXPECT_EQ(expected_digest, formatter.format(formatter_context, stream_info));
EXPECT_THAT(formatter.formatValue(formatter_context, stream_info),
ProtoEq(ValueUtil::stringValue(expected_digest)));

for (const std::string& command : {
"%REQUEST_HEADER_SHA256(authorization?x-request-id)%",
"%REQ_SHA256(authorization?x-request-id)%",
}) {
auto providers = SubstitutionFormatParser::parse(command);
ASSERT_TRUE(providers.ok()) << providers.status();
ASSERT_EQ(providers->size(), 1);
EXPECT_EQ(expected_digest, (*providers)[0]->format(formatter_context, stream_info));
}

Http::TestRequestHeaderMapImpl fallback_headers{{"x-request-id", "abc"}};
Context fallback_context;
fallback_context.setRequestHeaders(fallback_headers);
EXPECT_EQ(expected_digest, formatter.format(fallback_context, stream_info));

Http::TestRequestHeaderMapImpl empty_primary_headers{{"authorization", ""},
{"x-request-id", "abc"}};
Context empty_primary_context;
empty_primary_context.setRequestHeaders(empty_primary_headers);
EXPECT_EQ(expected_digest, formatter.format(empty_primary_context, stream_info));

Http::TestRequestHeaderMapImpl empty_headers{{"authorization", ""}, {"x-request-id", ""}};
Context empty_context;
empty_context.setRequestHeaders(empty_headers);
EXPECT_EQ(std::nullopt, formatter.format(empty_context, stream_info));
EXPECT_THAT(formatter.formatValue(empty_context, stream_info), ProtoEq(ValueUtil::nullValue()));

RequestHeaderSha256Formatter no_fallback("missing", "");
EXPECT_EQ(std::nullopt, no_fallback.format(formatter_context, stream_info));
EXPECT_EQ(std::nullopt, formatter.format({}, stream_info));

EXPECT_FALSE(SubstitutionFormatParser::parse("%REQ_SHA256%").ok());
EXPECT_FALSE(SubstitutionFormatParser::parse("%REQ_SHA256(authorization):8%").ok());
EXPECT_FALSE(
SubstitutionFormatParser::parse("%REQ_SHA256(authorization?fallback?another)%").ok());
}

TEST(SubstitutionFormatterTest, QueryParameterFormatter) {
StreamInfo::MockStreamInfo stream_info;
Http::TestRequestHeaderMapImpl request_header{{":method", "GET"}, {":path", "/path?x=xxxxxx"}};
Expand Down