All advisories
Draft

HTTP/2 METADATA Queue Grows Without Limit

envoyproxy/envoy

Affected packages

envoy other
Affected versions= c33d01624d5b173b5d6e5c0c0474d480cab69b7b
Patched versionsNot specified

Description

HTTP/2 METADATA Queue Grows Without Limit

Affected commit: c33d01624d
Sink: source/common/http/filter_manager.cc:1418

Summary

When HTTP/2 METADATA is enabled, Envoy queues each upstream metadata block until response headers are processed. An upstream peer can withhold headers and continuously send metadata, growing worker memory unboundedly until the stream closes or the worker is exhausted.

Detail

At filter_manager.cc:1418-1420, when a filter has not yet processed response headers (!processed_headers_) or has stopped iteration (stoppedAll()), metadata is saved into a per-filter vector:

if (!(*entry)->processed_headers_ || (*entry)->stoppedAll()) {
  (*entry)->getSavedResponseMetadata()->emplace_back(std::move(metadata_map_ptr));
  return;
}

getSavedResponseMetadata() returns a MetadataMapVector, which is defined in metadata_interface.h:47-49 as:

using VectorMetadataMapPtr = std::vector<MetadataMapPtr>;
class MetadataMapVector : public VectorMetadataMapPtr { ... };

This vector has no aggregate size limit. An upstream peer can withhold response headers (keeping processed_headers_ false) while continuously sending METADATA frames. Each frame appends to the vector, growing worker memory until the stream closes or the worker is exhausted.

Reproduce

#!/bin/bash
set -e

git clone --depth 1 https://github.com/envoyproxy/envoy.git /tmp/envoy-metadata
cd /tmp/envoy-metadata
echo "HEAD: $(git rev-parse HEAD)"

# Verify unbounded vector type
echo "=== MetadataMapVector is an unbounded std::vector ==="
grep -n 'VectorMetadataMapPtr\|class MetadataMapVector' envoy/http/metadata_interface.h

# Verify metadata saved without size check
echo ""
echo "=== Metadata queued without limit (filter_manager.cc:1414-1421) ==="
sed -n '1414,1421p' source/common/http/filter_manager.cc

# Verify saved_response_metadata_ has no size limit
echo ""
echo "=== Per-filter metadata storage (no limit) ==="
grep -n 'saved_response_metadata_\|getSavedResponseMetadata' source/common/http/filter_manager.h

rm -rf /tmp/envoy-metadata

Expected output (line numbers may shift):

HEAD: <resolved-HEAD>
=== MetadataMapVector is an unbounded std::vector ===
47:using VectorMetadataMapPtr = std::vector<MetadataMapPtr>;
49:class MetadataMapVector : public VectorMetadataMapPtr {

=== Metadata queued without limit (filter_manager.cc:1414-1421) ===
    // If the filter pointed by entry has stopped for all frame type, stores metadata and returns.
    // If the filter pointed by entry hasn't returned from encodeHeaders, stores newly added
    // metadata in case encodeHeaders returns StopAllIteration. ...
    if (!(*entry)->processed_headers_ || (*entry)->stoppedAll()) {
      (*entry)->getSavedResponseMetadata()->emplace_back(std::move(metadata_map_ptr));
      return;
    }

=== Per-filter metadata storage (no limit) ===
196:  MetadataMapVector* getSavedResponseMetadata() {
...
216:  std::unique_ptr<MetadataMapVector> saved_response_metadata_{nullptr};

With allow_metadata: true, a malicious upstream sends HTTP/2 METADATA frames before response headers. Each frame is queued in an unbounded std::vector per filter entry. The upstream withholds headers indefinitely while sending metadata, growing worker memory without any cap.

Credit

Zheng Yu @ Depthfirst