CVE-2021-43825 (C++, published 2022-02-22T23:15Z)
Description: value: Envoy is an open source edge and service proxy, designed for cloud-native applications. Sending a locally generated response must stop further processing of request or response data. Envoy tracks the amount of buffered request and response data and aborts the request if the amount of buffered data is over the limit by sending 413 or 500 responses. However when the buffer overflows while response is processed by the filter chain the operation may not be aborted correctly and result in accessing a freed memory block. If this happens Envoy will crash resulting in a denial of service.
Severity: MEDIUM (CVSS v3 base score: 7.5)
Weakness: CWE-416 \u2014 Use After Free
The product reuses or references memory after it has been freed. At some point afterward, the memory may be allocated again and saved in another pointer, while the original pointer references a location somewhere within the new allocation. Any operations using the original pointer are no longer valid because the memory "belongs" to the code that operates on the new pointer.
Repository: https://github.com/envoyproxy/envoy
Files changed:
source/common/http/conn_manager_impl.cc
source/common/http/filter_manager.cc
source/common/http/filter_manager.h
test/integration/BUILD
test/integration/filters/BUILD
test/integration/filters/buffer_continue_filter.cc
test/integration/protocol_integration_test.cc
Commit message: CVE-2021-43825

Response filter manager crash

Signed-off-by: Yan Avlasov <yavlasov@google.com>


Vulnerable code:
  if (stream.response_encoder_ != nullptr &&
      (!stream.filter_manager_.remoteComplete() || !stream.state_.codec_saw_local_complete_)) {
  bool request_complete = stream.filter_manager_.remoteComplete();
  if (!filter_manager_.remoteComplete()) {
bool ActiveStreamDecoderFilter::complete() { return parent_.state_.remote_complete_; }
  ASSERT(!state_.remote_complete_);
  state_.remote_complete_ = end_stream;
  bool canContinue() override { return true; }
  bool remoteComplete() const { return state_.remote_complete_; }
        : remote_complete_(false), local_complete_(false), has_1xx_headers_(false),
          created_filter_chain_(false), is_head_request_(false), is_grpc_request_(false),
          non_100_response_headers_encoded_(false), under_on_local_reply_(false),
          decoder_filter_chain_aborted_(false), encoder_filter_chain_aborted_(false),
          saw_downstream_reset_(false) {}

    bool remote_complete_ : 1;

Fixed code:
  if (stream.response_encoder_ != nullptr && (!stream.filter_manager_.remoteDecodeComplete() ||
                                              !stream.state_.codec_saw_local_complete_)) {
  bool request_complete = stream.filter_manager_.remoteDecodeComplete();
  if (!filter_manager_.remoteDecodeComplete()) {
bool ActiveStreamEncoderFilter::canContinue() {
  // As with ActiveStreamDecoderFilter::canContinue() make sure we do not
  // continue if a local reply has been sent.
  return !parent_.state_.remote_encode_complete_;
}

bool ActiveStreamDecoderFilter::complete() { return parent_.state_.remote_decode_complete_; }
  ASSERT(!state_.remote_decode_complete_);
  state_.remote_decode_complete_ = end_stream;
    ASSERT(!state_.remote_encode_complete_);
    state_.remote_encode_complete_ = true;
  ENVOY_STREAM_LOG(debug, "response data too large watermark exceeded", parent_);
  bool canContinue() override;
  bool remoteDecodeComplete() const { return state_.remote_decode_complete_; }
        : remote_encode_complete_(false), remote_decode_complete_(false), local_complete_(false),
          has_1xx_headers_(false), created_filter_chain_(false), is_head_request_(false),
          is_grpc_request_(false), non_100_response_headers_encoded_(false),
          under_on_local_reply_(false), decoder_filter_chain_aborted_(false),
          encoder_filter_chain_aborted_(false), saw_downstream_reset_(false) {}
    bool remote_encode_complete_ : 1;
    bool remote_decode_complete_ : 1;
        "//test/integration/filters:buffer_continue_filter_lib",
envoy_cc_test_library(
    name = "buffer_continue_filter_lib",
    srcs = [
        "buffer_continue_filter.cc",
    ],
    deps = [
        "//envoy/http:filter_interface",
        "//envoy/registry",
        "//envoy/server:filter_config_interface",
        "//source/extensions/filters/http/common:pass_through_filter_lib",
        "//test/extensions/filters/http/common:empty_http_filter_config_lib",
    ],
)

#include <string>

#include "envoy/http/filter.h"
#include "envoy/registry/registry.h"
#include "envoy/server/filter_config.h"

#include "source/extensions/filters/http/common/pass_through_filter.h"

#include "test/extensions/filters/http/common/empty_http_filter_config.h"

namespace Envoy {

// A filter that buffers until the limit is reached and then continues.
class BufferContinueStreamFilter : public Http::PassThroughFilter {
public:
  Http::FilterHeadersStatus decodeHeaders(Http::RequestHeaderMap&, bool end_stream) override {
    return end_stream ? Http::FilterHeadersStatus::Continue
                      : Http::FilterHeadersStatus::StopIteration;
  }

  Http::FilterDataStatus decodeData(Buffer::Instance&, bool end_stream) override {
    return end_stream ? Http::FilterDataStatus::Continue
                      : Http::FilterDataStatus::StopIterationAndBuffer;
  }

  Http::FilterHeadersStatus encodeHeaders(Http::ResponseHeaderMap& headers, bool) override {
    response_headers_ = &headers;
    return Http::FilterHeadersStatus::StopIteration;
  }

  Http::FilterDataStatus encodeData(Buffer::Instance& data, bool end_stream) override {
    data_total_ += data.length();

    const auto limit = encoder_callbacks_->encoderBufferLimit();
    const auto header_size = response_headers_->byteSize();

    if (limit && header_size + data_total_ > limit) {
      // Give up since we've reached the buffer limit, Envoy should generate
      // a 500 since it couldn't finished encoding.
      return Http::FilterDataStatus::Continue;
    }

    encoder_callbacks_->addEncodedData(data, false);

    if (!end_stream) {
      return Http::FilterDataStatus::StopIterationAndBuffer;
    }

    return Http::FilterDataStatus::Continue;
  }

private:
  Http::ResponseHeaderMap* response_headers_;
  uint64_t data_total_{0};
};

class BufferContinueFilterConfig : public Extensions::HttpFilters::Common::EmptyHttpFilterConfig {
public:
  BufferContinueFilterConfig() : EmptyHttpFilterConfig("buffer-continue-filter") {}

  Http::FilterFactoryCb createFilter(const std::string&,
                                     Server::Configuration::FactoryContext&) override {
    return [](Http::FilterChainFactoryCallbacks& callbacks) -> void {
      callbacks.addStreamFilter(std::make_shared<::Envoy::BufferContinueStreamFilter>());
    };
  }
};

// perform static registration
static Registry::RegisterFactory<BufferContinueFilterConfig,
                                 Server::Configuration::NamedHttpFilterConfigFactory>
    register_;

} // namespace Envoy
// Test buffering and then continuing after too many response bytes to buffer.
TEST_P(ProtocolIntegrationTest, BufferContinue) {
  // Bytes sent is configured for http/2 flow control windows.
  if (upstreamProtocol() != Http::CodecType::HTTP2) {
    return;
  }
  config_helper_.addConfigModifier(
      [&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager&
              hcm) -> void {
        auto* route_config = hcm.mutable_route_config();
        auto* virtual_host = route_config->mutable_virtual_hosts(0);
        auto* header = virtual_host->mutable_response_headers_to_add()->Add()->mutable_header();
        header->set_key("foo");
        header->set_value("bar");
      });

  useAccessLog();
  config_helper_.addFilter("{ name: buffer-continue-filter, typed_config: { \"@type\": "
                           "type.googleapis.com/google.protobuf.Empty } }");
  config_helper_.setBufferLimits(1024, 1024);
  initialize();

  // Send the request.
  codec_client_ = makeHttpConnection(lookupPort("http"));
  auto encoder_decoder = codec_client_->startRequest(default_request_headers_);
  auto downstream_request = &encoder_decoder.first;
  auto response = std::move(encoder_decoder.second);
  Buffer::OwnedImpl data("HTTP body content goes here");
  codec_client_->sendData(*downstream_request, data, true);
  waitForNextUpstreamRequest();

  // Send the response headers.
  upstream_request_->encodeHeaders(default_response_headers_, false);

  // Now send an overly large response body. At some point, too much data will
  // be buffered, the stream will be reset, and the connection will disconnect.
  upstream_request_->encodeData(512, false);
  upstream_request_->encodeData(1024 * 100, false);

  if (upstreamProtocol() == Http::CodecType::HTTP1) {
    ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect());
  } else {
    ASSERT_TRUE(upstream_request_->waitForReset());
    ASSERT_TRUE(fake_upstream_connection_->close());
    ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect());
  }

  ASSERT_TRUE(response->waitForEndStream());
  EXPECT_TRUE(response->complete());
  EXPECT_EQ("500", response->headers().getStatusValue());
}