Skip to content

Commit

Permalink
Validate that header names are only lowercase
Browse files Browse the repository at this point in the history
Motivation:

BHTTP requires that all header names are lowercase only, we should validate this.

Modifications:

- Implement validation to follow RFC
- Add unit tests

Result:

Correctly validate header names
  • Loading branch information
normanmaurer committed Aug 5, 2024
1 parent 9a6b509 commit 489e385
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
import io.netty.handler.codec.DefaultHeaders;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.HttpHeaderValidationUtil;
import io.netty.util.AsciiString;
import io.netty.util.ByteProcessor;

import static io.netty.util.AsciiString.isUpperCase;

/**
* {@link DefaultHttpHeaders} sub-type which allow to include custom
Expand All @@ -27,43 +31,83 @@ final class BinaryHttpHeaders extends DefaultHttpHeaders {

static BinaryHttpHeaders newHeaders(boolean validate) {
// For normal headers we need some special validator as we also need to support custom pseudo headers.
return new BinaryHttpHeaders(validate, BINARY_HTTP_VALIDATOR);
return new BinaryHttpHeaders(validate, BINARY_HTTP_HEADERS_VALIDATOR);
}

static BinaryHttpHeaders newTrailers(boolean validate) {
return new BinaryHttpHeaders(validate);
return new BinaryHttpHeaders(validate, BINARY_HTTP_TRAILERS_VALIDATOR);
}

// See https://lists.w3.org/Archives/Public/ietf-http-wg/2023JulSep/0017.html
private static final DefaultHeaders.NameValidator<CharSequence> BINARY_HTTP_VALIDATOR =
new DefaultHeaders.NameValidator<CharSequence>() {
private static final class BinaryHttpNameValidator implements DefaultHeaders.NameValidator<CharSequence> {

private static final ByteProcessor BINARY_HTTP_NAME_VALIDATOR_PROCESSOR = new ByteProcessor() {
@Override
public boolean process(byte value) {
return !isUpperCase(value);
}
};

private final boolean trailers;

BinaryHttpNameValidator(boolean trailers) {
this.trailers = trailers;
}

private static int checkUppercase(CharSequence name) {
if (name instanceof AsciiString) {
try {
return ((AsciiString) name).forEachByte(BINARY_HTTP_NAME_VALIDATOR_PROCESSOR);
} catch (Exception e) {
// Should never happen
throw new IllegalArgumentException("invalid header [" + name + ']', e);
}
} else {
for (int i = 0; i < name.length(); ++i) {
if (isUpperCase(name.charAt(i))) {
return i;
}
}
}
return -1;
}

@Override
public void validateName(CharSequence name) {
if (name != null && name.length() != 0) {
int index = HttpHeaderValidationUtil.validateToken(name);
if (index != -1) {
// If it's a pseudo-header the : will be on index 0.
if (index != 0 || !PseudoHeaderName.isPseudoHeaderPrefix(name.charAt(index))) {
// Pseudo headers are only allowed in headers but not in trailers.
if (trailers || index != 0 || !PseudoHeaderName.isPseudoHeaderPrefix(name.charAt(index))) {
throw new IllegalArgumentException("a header name can only contain \"token\" characters, "
+ "but found invalid character 0x" + Integer.toHexString(name.charAt(index))
+ " at index " + index + " of header '" + name + "'.");
} else if (PseudoHeaderName.isPseudoHeader(name)) {
}
if (PseudoHeaderName.isPseudoHeader(name)) {
throw new IllegalArgumentException("only custom pseudo-headers are allowed: '" + name + "'.");
}
}
// Check if the name contains uppercase chars as this is not allowed in HTTP2 and so not allowed in
// Binary HTTP:
// - https://www.rfc-editor.org/rfc/rfc9292.html#name-header-and-trailer-field-li
index = checkUppercase(name);
if (index != -1) {
throw new IllegalArgumentException("a header name can only contain \"lowercase\" characters, "
+ "but found invalid character 0x" + Integer.toHexString(name.charAt(index))
+ " at index " + index + " of header '" + name + "'.");
}
} else {
throw new IllegalArgumentException("empty headers are not allowed [" + name + ']');
}
}
};

private BinaryHttpHeaders(boolean validate) {
// See
super(validate);
}
// See https://lists.w3.org/Archives/Public/ietf-http-wg/2023JulSep/0017.html
private static final DefaultHeaders.NameValidator<CharSequence> BINARY_HTTP_HEADERS_VALIDATOR =
new BinaryHttpNameValidator(false);
private static final DefaultHeaders.NameValidator<CharSequence> BINARY_HTTP_TRAILERS_VALIDATOR =
new BinaryHttpNameValidator(true);

private BinaryHttpHeaders(boolean validate, DefaultHeaders.NameValidator<CharSequence> validator) {
// See
super(validate, validator);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;

import static org.junit.jupiter.api.Assertions.assertThrows;

Expand Down Expand Up @@ -55,4 +56,12 @@ public void emptyHeaderValue() {
HttpHeaders headers = BinaryHttpHeaders.newHeaders(true);
headers.set("name", "");
}

@ParameterizedTest
@ValueSource(booleans = { true, false })
public void uppercaseName(boolean trailers) {
HttpHeaders headers = trailers ? BinaryHttpHeaders.newTrailers(true) :
BinaryHttpHeaders.newHeaders(true);
assertThrows(IllegalArgumentException.class, () -> headers.set("UpperCase", "x"));
}
}

0 comments on commit 489e385

Please sign in to comment.