All advisories
Draft

Header Matching Comma-Join Defeats Route RBAC

envoyproxy/envoy

Affected packages

envoy other
Affected versions= c33d01624d5b173b5d6e5c0c0474d480cab69b7b
Patched versionsNot specified

Description

Header Matching Comma-Join Defeats Route RBAC

Affected commit: c33d01624d
Sink: source/common/http/header_utility.h:116

Summary

When individual header matching is disabled, Envoy joins repeated header values with commas before exact matching. An attacker can duplicate a routing header to prevent a protected route from matching, causing fall-through to an unprotected route.

Reproduce

#!/bin/bash
set -e

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

# Show matchesHeaders uses getAllOfHeaderAsString (comma-joined)
sed -n '115,130p' source/common/http/header_utility.h

# Show getAllOfHeaderAsString joins with comma
grep -n 'getAllOfHeaderAsString' source/common/http/header_utility.cc | head -5
grep -n -A3 'absl::StrJoin\|result_.emplace' source/common/http/header_utility.cc | head -15

echo ""
echo "=== Vulnerable code confirmed at HEAD ==="
echo "header_utility.h:116: matchesHeaders() calls"
echo "  getAllOfHeaderAsString() which joins repeated header values"
echo "  with commas before exact matching."
echo ""
echo "When a route uses header-based matching with exact values and"
echo "individual matching is disabled, an attacker can duplicate the"
echo "routing header to produce a comma-joined value that fails the"
echo "exact match, causing fall-through to an unprotected route."

rm -rf /tmp/envoy-header-join

Observed output (verified at c33d01624d):

HEAD: c33d01624d5b173b5d6e5c0c0474d480cab69b7b
    bool matchesHeaders(const HeaderMap& request_headers) const override {
      const auto header_value = getAllOfHeaderAsString(request_headers, name_);
      // If treat_missing_as_empty_ is false and there is no header value, most
      // matchers (other than HeaderDataPresentMatch) will just return false here.
      if (!treat_missing_as_empty_ && !header_value.result().has_value()) {
        return false;
      }

      // If the header does not have value and the result is not returned in the
      // code above, it means treat_missing_as_empty_ is set to true and we should
      // treat the header value as empty.
      absl::string_view value =
          header_value.result().has_value() ? header_value.result().value() : EMPTY_STRING;
      // Execute the specific matcher's code and invert if invert_match_ is set.
      return specificMatchesHeaders(value) != invert_match_;
    };

=== Vulnerable code confirmed at HEAD ===
header_utility.h:116: matchesHeaders() calls
  getAllOfHeaderAsString() which joins repeated header values
  with commas before exact matching.

When a route uses header-based matching with exact values and
individual matching is disabled, an attacker can duplicate the
routing header to produce a comma-joined value that fails the
exact match, causing fall-through to an unprotected route.

Credit

Zheng Yu @ Depthfirst