Skip to content

Commit

Permalink
test/fuzz: frame replay style testing and fuzzing for HTTP/2 headers. (
Browse files Browse the repository at this point in the history
…#6491)

In the fix patch for CVE-2019-9900, we introduced some basic HTTP/2
manual fuzzing, where single bytes were corrupted in a HEADERS frame, to
attempt to show that NUL/CR/LF were handled. However, testing that
relies on codec_impl_test has nghttp2 as both client and server. This
implies that Huffman coding may be present, and single byte corruptions
of 0x00 don't imply a NUL for example.

In this patch, we take a more principled approach and use artisinal
HEADERS frames that have no Huffman or dynamic table compression to
validate the above single byte corruption property.

A nice side effect of this is that we can derived from this
infrastructure stateless request/response HEADERS fuzzers that can cover
uncompressed (specifically no Huffman) paths, which is more likely to
provide a direct access to nghttp2 codec header sanitization logic.

Risk level: Low
Testing: Unit tests and ran both fuzzers under oss-fuzz Docker image.
  Seems reasonably fast and no crashes locally.

Signed-off-by: Harvey Tuch <htuch@google.com>
  • Loading branch information
htuch authored Apr 14, 2019
1 parent a5a1a99 commit 1e61a3f
Show file tree
Hide file tree
Showing 12 changed files with 631 additions and 56 deletions.
44 changes: 44 additions & 0 deletions test/common/http/http2/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ licenses(["notice"]) # Apache 2

load(
"//bazel:envoy_build_system.bzl",
"envoy_cc_fuzz_test",
"envoy_cc_test",
"envoy_cc_test_library",
"envoy_package",
Expand Down Expand Up @@ -55,6 +56,35 @@ envoy_cc_test(
],
)

envoy_cc_test_library(
name = "frame_replay_lib",
srcs = ["frame_replay.cc"],
hdrs = ["frame_replay.h"],
deps = [
"//source/common/common:hex_lib",
"//source/common/common:macros",
"//source/common/http/http2:codec_lib",
"//test/common/http:common_lib",
"//test/common/http/http2:codec_impl_test_util",
"//test/mocks/http:http_mocks",
"//test/mocks/network:network_mocks",
"//test/test_common:environment_lib",
"//test/test_common:utility_lib",
],
)

envoy_cc_test(
name = "frame_replay_test",
srcs = ["frame_replay_test.cc"],
data = [
"request_header_corpus/simple_example_huffman",
"request_header_corpus/simple_example_plain",
"response_header_corpus/simple_example_huffman",
"response_header_corpus/simple_example_plain",
],
deps = [":frame_replay_lib"],
)

envoy_cc_test(
name = "metadata_encoder_decoder_test",
srcs = ["metadata_encoder_decoder_test.cc"],
Expand All @@ -69,3 +99,17 @@ envoy_cc_test(
"//test/test_common:utility_lib",
],
)

envoy_cc_fuzz_test(
name = "response_header_fuzz_test",
srcs = ["response_header_fuzz_test.cc"],
corpus = "response_header_corpus",
deps = [":frame_replay_lib"],
)

envoy_cc_fuzz_test(
name = "request_header_fuzz_test",
srcs = ["request_header_fuzz_test.cc"],
corpus = "request_header_corpus",
deps = [":frame_replay_lib"],
)
56 changes: 0 additions & 56 deletions test/common/http/http2/codec_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,6 @@ namespace Http2 {
using Http2SettingsTuple = ::testing::tuple<uint32_t, uint32_t, uint32_t, uint32_t>;
using Http2SettingsTestParam = ::testing::tuple<Http2SettingsTuple, Http2SettingsTuple>;

constexpr Http2SettingsTuple
DefaultHttp2SettingsTuple(Http2Settings::DEFAULT_HPACK_TABLE_SIZE,
Http2Settings::DEFAULT_MAX_CONCURRENT_STREAMS,
Http2Settings::DEFAULT_MAX_CONCURRENT_STREAMS,
Http2Settings::DEFAULT_INITIAL_CONNECTION_WINDOW_SIZE);

class Http2CodecImplTestFixture {
public:
struct ConnectionWrapper {
Expand Down Expand Up @@ -89,9 +83,6 @@ class Http2CodecImplTestFixture {
if (corrupt_metadata_frame_) {
corruptMetadataFramePayload(data);
}
if (corrupt_at_offset_ >= 0) {
corruptAtOffset(data, corrupt_at_offset_, corrupt_with_char_);
}
server_wrapper_.dispatch(data, *server_);
}));
ON_CALL(server_connection_, write(_, _))
Expand Down Expand Up @@ -148,9 +139,6 @@ class Http2CodecImplTestFixture {
MockStreamCallbacks server_stream_callbacks_;
// Corrupt a metadata frame payload.
bool corrupt_metadata_frame_ = false;
// Corrupt frame at a given offset (if positive).
ssize_t corrupt_at_offset_{-1};
char corrupt_with_char_{'\0'};

uint32_t max_request_headers_kb_ = Http::DEFAULT_MAX_REQUEST_HEADERS_KB;
};
Expand Down Expand Up @@ -1041,50 +1029,6 @@ TEST_P(Http2CodecImplTest, TestCodecHeaderCompression) {
}
}

// Validate that nghttp2 rejects NUL/CR/LF as per
// https://httpwg.org/specs/rfc7540.html#rfc.section.10.3.
// TEST_P(Http2CodecImplTest, InvalidHeaderChars) {
// TODO(htuch): Write me. Http2CodecImplMutationTest basically covers this,
// but we could be a bit more specific and add a captured H2 HEADERS frame
// here and inject it with mutation of just the header value, ensuring we get
// the expected codec exception.
// }

class Http2CodecImplMutationTest : public ::testing::TestWithParam<::testing::tuple<char, int>>,
protected Http2CodecImplTestFixture {
public:
Http2CodecImplMutationTest()
: Http2CodecImplTestFixture(DefaultHttp2SettingsTuple, DefaultHttp2SettingsTuple) {}

void initialize() override {
corrupt_with_char_ = ::testing::get<0>(GetParam());
corrupt_at_offset_ = ::testing::get<1>(GetParam());
Http2CodecImplTestFixture::initialize();
}
};

INSTANTIATE_TEST_SUITE_P(Http2CodecImplMutationTest, Http2CodecImplMutationTest,
::testing::Combine(::testing::ValuesIn({'\0', '\r', '\n'}),
::testing::Range(0, 128)));

// Mutate an arbitrary offset in the HEADERS frame with NUL/CR/LF. This should
// either throw an exception or continue, but we shouldn't crash due to
// validHeaderString() ASSERTs.
TEST_P(Http2CodecImplMutationTest, HandleInvalidChars) {
initialize();

TestHeaderMapImpl request_headers;
request_headers.addCopy("foo", "barbaz");
HttpTestUtility::addDefaultHeaders(request_headers);
EXPECT_CALL(request_decoder_, decodeHeaders_(_, _)).Times(AnyNumber());
EXPECT_CALL(client_callbacks_, onGoAway()).Times(AnyNumber());
try {
request_encoder_->encodeHeaders(request_headers, true);
} catch (const CodecProtocolException& e) {
ENVOY_LOG_MISC(trace, "CodecProtocolException: {}", e.what());
}
}

} // namespace Http2
} // namespace Http
} // namespace Envoy
118 changes: 118 additions & 0 deletions test/common/http/http2/frame_replay.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#include "test/common/http/http2/frame_replay.h"

#include "common/common/hex.h"
#include "common/common/macros.h"

#include "test/common/http/common.h"
#include "test/test_common/environment.h"

namespace Envoy {
namespace Http {
namespace Http2 {

FileFrame::FileFrame(absl::string_view path) : api_(Api::createApiForTest()) {
const std::string contents = api_->fileSystem().fileReadToEnd(
TestEnvironment::runfilesPath("test/common/http/http2/" + std::string(path)));
frame_.resize(contents.size());
contents.copy(reinterpret_cast<char*>(frame_.data()), frame_.size());
}

std::unique_ptr<std::istream> FileFrame::istream() {
const std::string frame_string{reinterpret_cast<char*>(frame_.data()), frame_.size()};
return std::make_unique<std::istringstream>(frame_string);
}

const Frame& WellKnownFrames::clientConnectionPrefaceFrame() {
CONSTRUCT_ON_FIRST_USE(std::vector<uint8_t>,
{0x50, 0x52, 0x49, 0x20, 0x2a, 0x20, 0x48, 0x54, 0x54, 0x50, 0x2f, 0x32,
0x2e, 0x30, 0x0d, 0x0a, 0x0d, 0x0a, 0x53, 0x4d, 0x0d, 0x0a, 0x0d, 0x0a});
}

const Frame& WellKnownFrames::defaultSettingsFrame() {
CONSTRUCT_ON_FIRST_USE(std::vector<uint8_t>,
{0x00, 0x00, 0x0c, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04,
0x7f, 0xff, 0xff, 0xff, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00});
}

const Frame& WellKnownFrames::initialWindowUpdateFrame() {
CONSTRUCT_ON_FIRST_USE(std::vector<uint8_t>, {0x00, 0x00, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00,
0x00, 0x0f, 0xff, 0x00, 0x01});
}

void FrameUtils::fixupHeaders(Frame& frame) {
constexpr size_t frame_header_len = 9; // from RFC 7540
while (frame.size() < frame_header_len) {
frame.emplace_back(0x00);
}
size_t headers_len = frame.size() - frame_header_len;
frame[2] = headers_len & 0xff;
headers_len >>= 8;
frame[1] = headers_len & 0xff;
headers_len >>= 8;
frame[0] = headers_len & 0xff;
// HEADERS frame with END_STREAM | END_HEADERS for stream 1.
size_t offset = 3;
for (const uint8_t b : {0x01, 0x05, 0x00, 0x00, 0x00, 0x01}) {
frame[offset++] = b;
}
}

CodecFrameInjector::CodecFrameInjector(const std::string& injector_name)
: injector_name_(injector_name) {
settings_.hpack_table_size_ = Http2Settings::DEFAULT_HPACK_TABLE_SIZE;
settings_.max_concurrent_streams_ = Http2Settings::DEFAULT_MAX_CONCURRENT_STREAMS;
settings_.initial_stream_window_size_ = Http2Settings::DEFAULT_INITIAL_STREAM_WINDOW_SIZE;
settings_.initial_connection_window_size_ = Http2Settings::DEFAULT_INITIAL_CONNECTION_WINDOW_SIZE;
settings_.allow_metadata_ = false;
}

ClientCodecFrameInjector::ClientCodecFrameInjector() : CodecFrameInjector("server") {
auto client = std::make_unique<TestClientConnectionImpl>(client_connection_, client_callbacks_,
stats_store_, settings_,
Http::DEFAULT_MAX_REQUEST_HEADERS_KB);
request_encoder_ = &client->newStream(response_decoder_);
connection_ = std::move(client);
ON_CALL(client_connection_, write(_, _))
.WillByDefault(Invoke([&](Buffer::Instance& data, bool) -> void {
ENVOY_LOG_MISC(
trace, "client write: {}",
Hex::encode(static_cast<uint8_t*>(data.linearize(data.length())), data.length()));
data.drain(data.length());
}));
request_encoder_->getStream().addCallbacks(client_stream_callbacks_);
// Setup a single stream to inject frames as a reply to.
TestHeaderMapImpl request_headers;
HttpTestUtility::addDefaultHeaders(request_headers);
request_encoder_->encodeHeaders(request_headers, true);
}

ServerCodecFrameInjector::ServerCodecFrameInjector() : CodecFrameInjector("client") {
connection_ = std::make_unique<TestServerConnectionImpl>(server_connection_, server_callbacks_,
stats_store_, settings_,
Http::DEFAULT_MAX_REQUEST_HEADERS_KB);
EXPECT_CALL(server_callbacks_, newStream(_, _))
.WillRepeatedly(Invoke([&](StreamEncoder& encoder, bool) -> StreamDecoder& {
encoder.getStream().addCallbacks(server_stream_callbacks_);
return request_decoder_;
}));
ON_CALL(server_connection_, write(_, _))
.WillByDefault(Invoke([&](Buffer::Instance& data, bool) -> void {
ENVOY_LOG_MISC(
trace, "server write: {}",
Hex::encode(static_cast<uint8_t*>(data.linearize(data.length())), data.length()));
data.drain(data.length());
}));
}

void CodecFrameInjector::write(const Frame& frame) {
Buffer::OwnedImpl buffer;
buffer.add(frame.data(), frame.size());
ENVOY_LOG_MISC(trace, "{} write: {}", injector_name_, Hex::encode(frame.data(), frame.size()));
while (buffer.length() > 0) {
connection_->dispatch(buffer);
}
}

} // namespace Http2
} // namespace Http
} // namespace Envoy
91 changes: 91 additions & 0 deletions test/common/http/http2/frame_replay.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#include <cstdint>
#include <memory>
#include <vector>

#include "common/stats/isolated_store_impl.h"

#include "test/common/http/http2/codec_impl_test_util.h"
#include "test/mocks/http/mocks.h"
#include "test/mocks/network/mocks.h"
#include "test/test_common/utility.h"

#include "absl/strings/string_view.h"
#include "gmock/gmock.h"

namespace Envoy {
namespace Http {
namespace Http2 {

// A byte vector representation of an HTTP/2 frame.
typedef std::vector<uint8_t> Frame;

// An HTTP/2 frame derived from a file location.
class FileFrame {
public:
FileFrame(absl::string_view path);

Frame& frame() { return frame_; }
std::unique_ptr<std::istream> istream();

Frame frame_;
Api::ApiPtr api_;
};

// Some standards HTTP/2 frames for setting up a connection. The contents for these and the seed
// corpus were captured via logging the hex bytes in codec_impl_test's write() connection mocks in
// setupDefaultConnectionMocks().
class WellKnownFrames {
public:
static const Frame& clientConnectionPrefaceFrame();
static const Frame& defaultSettingsFrame();
static const Frame& initialWindowUpdateFrame();
};

class FrameUtils {
public:
// Modify a given frame so that it has the HTTP/2 frame header for a valid
// HEADERS frame.
static void fixupHeaders(Frame& frame);
};

class CodecFrameInjector {
public:
CodecFrameInjector(const std::string& injector_name);

void write(const Frame& frame);

Http2Settings settings_;
std::unique_ptr<Http::Connection> connection_;
Stats::IsolatedStoreImpl stats_store_;
const std::string injector_name_;
};

// Wrapper for HTTP/2 client codec supporting injection of frames and expecting on
// the behaviors of callbacks and the request decoder.
class ClientCodecFrameInjector : public CodecFrameInjector {
public:
ClientCodecFrameInjector();

::testing::NiceMock<Network::MockConnection> client_connection_;
MockConnectionCallbacks client_callbacks_;
MockStreamDecoder response_decoder_;
StreamEncoder* request_encoder_;
MockStreamCallbacks client_stream_callbacks_;
};

// Wrapper for HTTP/2 server codec supporting injection of frames and expecting on
// the behaviors of callbacks and the request decoder.
class ServerCodecFrameInjector : public CodecFrameInjector {
public:
ServerCodecFrameInjector();

::testing::NiceMock<Network::MockConnection> server_connection_;
MockServerConnectionCallbacks server_callbacks_;
std::unique_ptr<TestServerConnectionImpl> server_;
MockStreamDecoder request_decoder_;
MockStreamCallbacks server_stream_callbacks_;
};

} // namespace Http2
} // namespace Http
} // namespace Envoy
Loading

0 comments on commit 1e61a3f

Please sign in to comment.