Skip to content

Commit 9a1d427

Browse files
committed
xds: Enforce unconditional validation for ext_proc header values and remove HeaderValue.isValid
Per gRFC A93, the `disallow_is_error` configuration applies strictly to mutation operation rules (attempting to modify disallowed header keys). Conversely, malformed or invalid header values received from `ext_proc` represent protocol/response errors and must unconditionally throw an `IllegalArgumentException`. Key changes: - Remove `isValid` field and `createInvalid` factory from `HeaderValue` struct. - Add unconditional `validateHeaderKey` and `validateHeaderValue` checks throwing `IllegalArgumentException` in `HeaderValueValidationUtils`. - Restrict `HeaderValueValidationUtils.isDisallowed` exclusively to mutation operation rule checks (`host`, `:authority`, `:path`, `grpc-*`, uppercase keys). - Update `ExternalProcessorClientInterceptor` and `GrpcServiceConfigParser` to enforce unconditional validation. - Update unit tests across `grpc-xds` to verify unconditional failure on malformed headers even when `disallow_is_error` is false. And other minor review comments.
1 parent da3d2b1 commit 9a1d427

16 files changed

Lines changed: 163 additions & 263 deletions

xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -623,36 +623,40 @@ private void applyHeaderMutations(Metadata metadata,
623623
for (io.envoyproxy.envoy.config.core.v3.HeaderValueOption protoOption
624624
: mutation.getSetHeadersList()) {
625625
io.envoyproxy.envoy.config.core.v3.HeaderValue protoHeader = protoOption.getHeader();
626-
HeaderValue headerValue;
627-
626+
String key = protoHeader.getKey();
627+
HeaderValueValidationUtils.validateHeaderKey(key);
628+
628629
ByteString rawBytes = protoHeader.getRawValue();
629630
if (rawBytes.isEmpty()) {
630631
rawBytes = ByteString.copyFromUtf8(protoHeader.getValue());
631632
}
632633

633634
if (rawBytes.size() > HeaderValueValidationUtils.MAX_HEADER_LENGTH) {
634-
headerValue = HeaderValue.createInvalid(protoHeader.getKey());
635-
} else if (protoHeader.getKey().endsWith(Metadata.BINARY_HEADER_SUFFIX)) {
636-
try {
637-
byte[] decodedBytes = BaseEncoding.base64().decode(rawBytes.toStringUtf8());
638-
headerValue = HeaderValue.create(
639-
protoHeader.getKey(), ByteString.copyFrom(decodedBytes));
640-
} catch (IllegalArgumentException e) {
641-
// Mark as invalid so HeaderMutationFilter can either silently ignore it or
642-
// throw an exception based on the disallow_is_error configuration.
643-
headerValue = HeaderValue.createInvalid(protoHeader.getKey());
644-
}
635+
throw new IllegalArgumentException(
636+
"Header value length exceeds maximum allowed length: " + rawBytes.size());
637+
}
638+
639+
HeaderValue headerValue;
640+
if (key.endsWith(Metadata.BINARY_HEADER_SUFFIX)) {
641+
byte[] decodedBytes = BaseEncoding.base64().decode(rawBytes.toStringUtf8());
642+
headerValue = HeaderValue.create(key, ByteString.copyFrom(decodedBytes));
645643
} else {
646-
headerValue = HeaderValue.create(protoHeader.getKey(), rawBytes.toStringUtf8());
644+
headerValue = HeaderValue.create(key, rawBytes.toStringUtf8());
647645
}
648646
headersToModify.add(HeaderValueOption.create(
649647
headerValue,
650648
HeaderValueOption.HeaderAppendAction.valueOf(protoOption.getAppendAction().name())));
651649
}
652650

651+
ImmutableList.Builder<String> headersToRemove = ImmutableList.builder();
652+
for (String headerToRemove : mutation.getRemoveHeadersList()) {
653+
HeaderValueValidationUtils.validateHeaderKey(headerToRemove);
654+
headersToRemove.add(headerToRemove);
655+
}
656+
653657
HeaderMutations mutations = HeaderMutations.create(
654658
headersToModify.build(),
655-
ImmutableList.copyOf(mutation.getRemoveHeadersList()));
659+
headersToRemove.build());
656660

657661
HeaderMutations filteredMutations = mutationFilter.filter(mutations);
658662
mutator.applyMutations(filteredMutations, metadata);

xds/src/main/java/io/grpc/xds/ExternalProcessorFilter.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import io.envoyproxy.envoy.extensions.filters.http.ext_proc.v3.HeaderForwardingRules;
3333
import io.envoyproxy.envoy.extensions.filters.http.ext_proc.v3.ProcessingMode;
3434
import io.grpc.ClientInterceptor;
35+
import io.grpc.internal.GrpcUtil;
3536
import io.grpc.xds.Filter.FilterConfigParseContext;
3637
import io.grpc.xds.Filter.FilterContext;
3738
import io.grpc.xds.internal.MatcherParser;
@@ -82,7 +83,7 @@ public String[] typeUrls() {
8283

8384
@Override
8485
public boolean isClientFilter() {
85-
return true;
86+
return GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_EXT_PROC_ON_CLIENT", false);
8687
}
8788

8889
@Override

xds/src/main/java/io/grpc/xds/FilterRegistry.java

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package io.grpc.xds;
1818

1919
import com.google.common.annotations.VisibleForTesting;
20-
import io.grpc.internal.GrpcUtil;
2120
import java.util.HashMap;
2221
import java.util.Map;
2322
import javax.annotation.Nullable;
@@ -39,10 +38,8 @@ static synchronized FilterRegistry getDefaultRegistry() {
3938
new FaultFilter.Provider(),
4039
new RouterFilter.Provider(),
4140
new RbacFilter.Provider(),
42-
new GcpAuthenticationFilter.Provider());
43-
if (GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_EXT_PROC_ON_CLIENT", false)) {
44-
instance.register(new ExternalProcessorFilter.Provider());
45-
}
41+
new GcpAuthenticationFilter.Provider(),
42+
new ExternalProcessorFilter.Provider());
4643
}
4744
return instance;
4845
}

xds/src/main/java/io/grpc/xds/GrpcServiceConfigParser.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,14 @@ public static GrpcServiceConfig parse(GrpcService grpcServiceProto,
9898
.getInitialMetadataList()) {
9999
String key = header.getKey();
100100
HeaderValue headerValue;
101-
if (key.endsWith(Metadata.BINARY_HEADER_SUFFIX)) {
102-
headerValue = HeaderValue.create(key, header.getRawValue());
103-
} else {
104-
headerValue = HeaderValue.create(key, header.getValue());
101+
try {
102+
if (key.endsWith(Metadata.BINARY_HEADER_SUFFIX)) {
103+
headerValue = HeaderValue.create(key, header.getRawValue());
104+
} else {
105+
headerValue = HeaderValue.create(key, header.getValue());
106+
}
107+
} catch (IllegalArgumentException e) {
108+
throw new GrpcServiceParseException("Invalid initial metadata header: " + key, e);
105109
}
106110
if (HeaderValueValidationUtils.isDisallowed(headerValue)) {
107111
throw new GrpcServiceParseException("Invalid initial metadata header: " + key);

xds/src/main/java/io/grpc/xds/XdsNameResolver.java

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -739,10 +739,6 @@ private void updateActiveFilters(@Nullable List<NamedFilterConfig> filterConfigs
739739
Set<String> filtersToShutdown = new HashSet<>(activeFilters.keySet());
740740
for (NamedFilterConfig namedFilter : filterConfigs) {
741741
String typeUrl = namedFilter.filterConfig.typeUrl();
742-
if (typeUrl.equals(ExternalProcessorFilter.TYPE_URL)
743-
&& !GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_EXT_PROC_ON_CLIENT", false)) {
744-
continue;
745-
}
746742
String filterKey = namedFilter.filterStateKey();
747743

748744
Filter.Provider provider = filterRegistry.get(typeUrl);
@@ -893,12 +889,6 @@ private ClientInterceptor createFilters(
893889

894890
ImmutableList.Builder<ClientInterceptor> filterInterceptors = ImmutableList.builder();
895891
for (NamedFilterConfig namedFilter : filterConfigs) {
896-
String typeUrl = namedFilter.filterConfig.typeUrl();
897-
if (typeUrl.equals(ExternalProcessorFilter.TYPE_URL)) {
898-
if (!GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_EXT_PROC_ON_CLIENT", false)) {
899-
continue;
900-
}
901-
}
902892
String name = namedFilter.name;
903893
FilterConfig config = namedFilter.filterConfig;
904894
FilterConfig overrideConfig = selectedOverrideConfigs.get(name);

xds/src/main/java/io/grpc/xds/XdsNameResolverProvider.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public XdsNameResolverProvider() {
6363
}
6464

6565
private XdsNameResolverProvider(String scheme,
66-
@Nullable Map<String, ?> bootstrapOverride) {
66+
@Nullable Map<String, ?> bootstrapOverride) {
6767
this.scheme = checkNotNull(scheme, "scheme");
6868
this.bootstrapOverride = bootstrapOverride;
6969
}
@@ -73,7 +73,7 @@ private XdsNameResolverProvider(String scheme,
7373
* and bootstrap.
7474
*/
7575
public static XdsNameResolverProvider createForTest(String scheme,
76-
@Nullable Map<String, ?> bootstrapOverride) {
76+
@Nullable Map<String, ?> bootstrapOverride) {
7777
return new XdsNameResolverProvider(scheme, bootstrapOverride);
7878
}
7979

xds/src/main/java/io/grpc/xds/internal/grpcservice/ChannelCredsConfig.java

Lines changed: 0 additions & 27 deletions
This file was deleted.

xds/src/main/java/io/grpc/xds/internal/grpcservice/HeaderValue.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@
2828
public abstract class HeaderValue {
2929

3030
public static HeaderValue create(String key, String value) {
31+
HeaderValueValidationUtils.validateHeaderValue(key, value);
3132
return new AutoValue_HeaderValue(key, Optional.of(value), Optional.empty(), true);
3233
}
3334

3435
public static HeaderValue create(String key, ByteString rawValue) {
36+
HeaderValueValidationUtils.validateHeaderValue(key, rawValue);
3537
return new AutoValue_HeaderValue(key, Optional.empty(), Optional.of(rawValue), true);
3638
}
3739

xds/src/main/java/io/grpc/xds/internal/grpcservice/HeaderValueValidationUtils.java

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,47 @@ public final class HeaderValueValidationUtils {
2828
private HeaderValueValidationUtils() {}
2929

3030
/**
31-
* Returns true if the header key is disallowed for mutations or validation.
31+
* Validates that the header key is non-empty and within allowed length.
32+
* Throws {@link IllegalArgumentException} if invalid.
33+
*/
34+
public static void validateHeaderKey(String key) {
35+
if (key == null || key.isEmpty() || key.length() > MAX_HEADER_LENGTH) {
36+
throw new IllegalArgumentException("Invalid header key: " + key);
37+
}
38+
}
39+
40+
/**
41+
* Validates that the header value is within allowed length and contains valid ASCII characters.
42+
* Throws {@link IllegalArgumentException} if invalid.
43+
*/
44+
public static void validateHeaderValue(String key, String value) {
45+
validateHeaderKey(key);
46+
if (value == null || value.length() > MAX_HEADER_LENGTH) {
47+
throw new IllegalArgumentException("Header value length exceeds maximum allowed length");
48+
}
49+
if (!key.endsWith("-bin") && !isValidAsciiHeaderValue(value)) {
50+
throw new IllegalArgumentException(
51+
"Invalid ASCII characters in header value for key: " + key);
52+
}
53+
}
54+
55+
/**
56+
* Validates that the raw header value is within allowed length and contains valid ASCII
57+
* characters. Throws {@link IllegalArgumentException} if invalid.
58+
*/
59+
public static void validateHeaderValue(String key, ByteString rawValue) {
60+
validateHeaderKey(key);
61+
if (rawValue == null || rawValue.size() > MAX_HEADER_LENGTH) {
62+
throw new IllegalArgumentException("Header value length exceeds maximum allowed length");
63+
}
64+
if (!key.endsWith("-bin") && !isValidAsciiHeaderValue(rawValue.toStringUtf8())) {
65+
throw new IllegalArgumentException(
66+
"Invalid ASCII characters in header value for key: " + key);
67+
}
68+
}
69+
70+
/**
71+
* Returns true if the header key is disallowed for mutations.
3272
*
3373
* @param key The header key (e.g., "content-type")
3474
*/
@@ -49,30 +89,12 @@ public static boolean isDisallowed(String key) {
4989
}
5090

5191
/**
52-
* Returns true if the header value is disallowed.
92+
* Returns true if the header is disallowed for mutations.
5393
*
54-
* @param header The HeaderValue containing key and values
94+
* @param header The HeaderValue
5595
*/
5696
public static boolean isDisallowed(HeaderValue header) {
57-
if (!header.isValid()) {
58-
return true;
59-
}
60-
if (isDisallowed(header.key())) {
61-
return true;
62-
}
63-
if (header.value().isPresent()) {
64-
String val = header.value().get();
65-
if (!header.key().endsWith("-bin") && !isValidAsciiHeaderValue(val)) {
66-
return true;
67-
}
68-
}
69-
if (header.rawValue().isPresent()) {
70-
ByteString rawVal = header.rawValue().get();
71-
if (!header.key().endsWith("-bin") && !isValidAsciiHeaderValue(rawVal.toStringUtf8())) {
72-
return true;
73-
}
74-
}
75-
return false;
97+
return isDisallowed(header.key());
7698
}
7799

78100
/**

0 commit comments

Comments
 (0)