Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTP/3: Header limits and validation #31928

Merged
merged 2 commits into from
Apr 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions src/Servers/Kestrel/Core/src/CoreStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -390,34 +390,34 @@
<data name="Http2ErrorStreamIdle" xml:space="preserve">
<value>The client sent a {frameType} frame to idle stream ID {streamId}.</value>
</data>
<data name="Http2ErrorTrailersContainPseudoHeaderField" xml:space="preserve">
<data name="HttpErrorTrailersContainPseudoHeaderField" xml:space="preserve">
<value>The client sent trailers containing one or more pseudo-header fields.</value>
</data>
<data name="Http2ErrorHeaderNameUppercase" xml:space="preserve">
<data name="HttpErrorHeaderNameUppercase" xml:space="preserve">
<value>The client sent a header with uppercase characters in its name.</value>
</data>
<data name="Http2ErrorTrailerNameUppercase" xml:space="preserve">
<data name="HttpErrorTrailerNameUppercase" xml:space="preserve">
<value>The client sent a trailer with uppercase characters in its name.</value>
</data>
<data name="Http2ErrorHeadersWithTrailersNoEndStream" xml:space="preserve">
<value>The client sent a HEADERS frame containing trailers without setting the END_STREAM flag.</value>
</data>
<data name="Http2ErrorMissingMandatoryPseudoHeaderFields" xml:space="preserve">
<data name="HttpErrorMissingMandatoryPseudoHeaderFields" xml:space="preserve">
<value>Request headers missing one or more mandatory pseudo-header fields.</value>
</data>
<data name="Http2ErrorPseudoHeaderFieldAfterRegularHeaders" xml:space="preserve">
<data name="HttpErrorPseudoHeaderFieldAfterRegularHeaders" xml:space="preserve">
<value>Pseudo-header field found in request headers after regular header fields.</value>
</data>
<data name="Http2ErrorUnknownPseudoHeaderField" xml:space="preserve">
<data name="HttpErrorUnknownPseudoHeaderField" xml:space="preserve">
<value>Request headers contain unknown pseudo-header field.</value>
</data>
<data name="Http2ErrorResponsePseudoHeaderField" xml:space="preserve">
<data name="HttpErrorResponsePseudoHeaderField" xml:space="preserve">
<value>Request headers contain response-specific pseudo-header field.</value>
</data>
<data name="Http2ErrorDuplicatePseudoHeaderField" xml:space="preserve">
<data name="HttpErrorDuplicatePseudoHeaderField" xml:space="preserve">
<value>Request headers contain duplicate pseudo-header field.</value>
</data>
<data name="Http2ErrorConnectionSpecificHeaderField" xml:space="preserve">
<data name="HttpErrorConnectionSpecificHeaderField" xml:space="preserve">
<value>Request headers contain connection-specific header field.</value>
</data>
<data name="AuthenticationFailed" xml:space="preserve">
Expand Down Expand Up @@ -680,4 +680,4 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
<data name="Http3ControlStreamErrorMultipleInboundStreams" xml:space="preserve">
<value>The client created multiple inbound {streamType} streams for the connection.</value>
</data>
</root>
</root>
18 changes: 9 additions & 9 deletions src/Servers/Kestrel/Core/src/Internal/Http2/Http2Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1026,7 +1026,7 @@ private void StartStream()
// All HTTP/2 requests MUST include exactly one valid value for the :method, :scheme, and :path pseudo-header
// fields, unless it is a CONNECT request (Section 8.3). An HTTP request that omits mandatory pseudo-header
// fields is malformed (Section 8.1.2.6).
throw new Http2StreamErrorException(_currentHeadersStream.StreamId, CoreStrings.Http2ErrorMissingMandatoryPseudoHeaderFields, Http2ErrorCode.PROTOCOL_ERROR);
throw new Http2StreamErrorException(_currentHeadersStream.StreamId, CoreStrings.HttpErrorMissingMandatoryPseudoHeaderFields, Http2ErrorCode.PROTOCOL_ERROR);
}

if (_clientActiveStreamCount == _serverSettings.MaxConcurrentStreams)
Expand Down Expand Up @@ -1327,7 +1327,7 @@ private void ValidateHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)

if (IsConnectionSpecificHeaderField(name, value))
{
throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorConnectionSpecificHeaderField, Http2ErrorCode.PROTOCOL_ERROR);
throw new Http2ConnectionErrorException(CoreStrings.HttpErrorConnectionSpecificHeaderField, Http2ErrorCode.PROTOCOL_ERROR);
}

// http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2
Expand All @@ -1338,11 +1338,11 @@ private void ValidateHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
{
if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers)
{
throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorTrailerNameUppercase, Http2ErrorCode.PROTOCOL_ERROR);
throw new Http2ConnectionErrorException(CoreStrings.HttpErrorTrailerNameUppercase, Http2ErrorCode.PROTOCOL_ERROR);
}
else
{
throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorHeaderNameUppercase, Http2ErrorCode.PROTOCOL_ERROR);
throw new Http2ConnectionErrorException(CoreStrings.HttpErrorHeaderNameUppercase, Http2ErrorCode.PROTOCOL_ERROR);
}
}
}
Expand Down Expand Up @@ -1398,13 +1398,13 @@ implementations to these vulnerabilities.*/
// All pseudo-header fields MUST appear in the header block before regular header fields.
// Any request or response that contains a pseudo-header field that appears in a header
// block after a regular header field MUST be treated as malformed (Section 8.1.2.6).
throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorPseudoHeaderFieldAfterRegularHeaders, Http2ErrorCode.PROTOCOL_ERROR);
throw new Http2ConnectionErrorException(CoreStrings.HttpErrorPseudoHeaderFieldAfterRegularHeaders, Http2ErrorCode.PROTOCOL_ERROR);
}

if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers)
{
// Pseudo-header fields MUST NOT appear in trailers.
throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorTrailersContainPseudoHeaderField, Http2ErrorCode.PROTOCOL_ERROR);
throw new Http2ConnectionErrorException(CoreStrings.HttpErrorTrailersContainPseudoHeaderField, Http2ErrorCode.PROTOCOL_ERROR);
}

_requestHeaderParsingState = RequestHeaderParsingState.PseudoHeaderFields;
Expand All @@ -1413,21 +1413,21 @@ implementations to these vulnerabilities.*/
{
// Endpoints MUST treat a request or response that contains undefined or invalid pseudo-header
// fields as malformed (Section 8.1.2.6).
throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorUnknownPseudoHeaderField, Http2ErrorCode.PROTOCOL_ERROR);
throw new Http2ConnectionErrorException(CoreStrings.HttpErrorUnknownPseudoHeaderField, Http2ErrorCode.PROTOCOL_ERROR);
}

if (headerField == PseudoHeaderFields.Status)
{
// Pseudo-header fields defined for requests MUST NOT appear in responses; pseudo-header fields
// defined for responses MUST NOT appear in requests.
throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorResponsePseudoHeaderField, Http2ErrorCode.PROTOCOL_ERROR);
throw new Http2ConnectionErrorException(CoreStrings.HttpErrorResponsePseudoHeaderField, Http2ErrorCode.PROTOCOL_ERROR);
}

if ((_parsedPseudoHeaderFields & headerField) == headerField)
{
// http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2.3
// All HTTP/2 requests MUST include exactly one valid value for the :method, :scheme, and :path pseudo-header fields
throw new Http2ConnectionErrorException(CoreStrings.Http2ErrorDuplicatePseudoHeaderField, Http2ErrorCode.PROTOCOL_ERROR);
throw new Http2ConnectionErrorException(CoreStrings.HttpErrorDuplicatePseudoHeaderField, Http2ErrorCode.PROTOCOL_ERROR);
}

if (headerField == PseudoHeaderFields.Method)
Expand Down
67 changes: 45 additions & 22 deletions src/Servers/Kestrel/Core/src/Internal/Http3/Http3Stream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttpHeadersHandler,
private static ReadOnlySpan<byte> TrailersBytes => new byte[8] { (byte)'t', (byte)'r', (byte)'a', (byte)'i', (byte)'l', (byte)'e', (byte)'r', (byte)'s' };
private static ReadOnlySpan<byte> ConnectBytes => new byte[7] { (byte)'C', (byte)'O', (byte)'N', (byte)'N', (byte)'E', (byte)'C', (byte)'T' };

private static readonly PseudoHeaderFields _mandatoryRequestPseudoHeaderFields =
PseudoHeaderFields.Method | PseudoHeaderFields.Path | PseudoHeaderFields.Scheme;

private readonly Http3FrameWriter _frameWriter;
private readonly Http3OutputProducer _http3Output;
private int _isClosed;
Expand All @@ -42,6 +45,7 @@ internal abstract partial class Http3Stream : HttpProtocol, IHttpHeadersHandler,
private readonly Http3RawFrame _incomingFrame = new Http3RawFrame();
protected RequestHeaderParsingState _requestHeaderParsingState;
private PseudoHeaderFields _parsedPseudoHeaderFields;
private int _totalParsedHeaderSize;
private bool _isMethodConnect;

private readonly Http3Connection _http3Connection;
Expand Down Expand Up @@ -148,7 +152,14 @@ public void OnStaticIndexedHeader(int index, ReadOnlySpan<byte> value)

public override void OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
{
// TODO MaxRequestHeadersTotalSize?
// https://tools.ietf.org/html/rfc7540#section-6.5.2
// "The value is based on the uncompressed size of header fields, including the length of the name and value in octets plus an overhead of 32 octets for each header field.";
_totalParsedHeaderSize += HeaderField.RfcOverhead + name.Length + value.Length;
if (_totalParsedHeaderSize > _context.ServiceContext.ServerOptions.Limits.MaxRequestHeadersTotalSize)
{
throw new Http3StreamErrorException(CoreStrings.BadRequest_HeadersExceedMaxTotalSize, Http3ErrorCode.RequestRejected);
}

ValidateHeader(name, value);
try
{
Expand All @@ -165,23 +176,22 @@ public override void OnHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
}
catch (Microsoft.AspNetCore.Http.BadHttpRequestException bre)
{
throw new Http3StreamErrorException(bre.Message, Http3ErrorCode.ProtocolError);
throw new Http3StreamErrorException(bre.Message, Http3ErrorCode.MessageError);
}
catch (InvalidOperationException)
{
throw new Http3StreamErrorException(CoreStrings.BadRequest_MalformedRequestInvalidHeaders, Http3ErrorCode.ProtocolError);
throw new Http3StreamErrorException(CoreStrings.BadRequest_MalformedRequestInvalidHeaders, Http3ErrorCode.MessageError);
}
}

private void ValidateHeader(ReadOnlySpan<byte> name, ReadOnlySpan<byte> value)
{
// http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2.1
/*
Intermediaries that process HTTP requests or responses (i.e., any
intermediary not acting as a tunnel) MUST NOT forward a malformed
request or response. Malformed requests or responses that are
detected MUST be treated as a stream error (Section 5.4.2) of type
PROTOCOL_ERROR.
Intermediaries that process HTTP requests or responses
(i.e., any intermediary not acting as a tunnel) MUST NOT forward a
malformed request or response. Malformed requests or responses that
are detected MUST be treated as a stream error of type H3_MESSAGE_ERROR.

For malformed requests, a server MAY send an HTTP response prior to
closing or resetting the stream. Clients MUST NOT accept a malformed
Expand All @@ -193,39 +203,43 @@ implementations to these vulnerabilities.*/
{
if (_requestHeaderParsingState == RequestHeaderParsingState.Headers)
{
// https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1.1.1-4
// All pseudo-header fields MUST appear in the header block before regular header fields.
// Any request or response that contains a pseudo-header field that appears in a header
// block after a regular header field MUST be treated as malformed (Section 8.1.2.6).
throw new Http3StreamErrorException(CoreStrings.Http2ErrorPseudoHeaderFieldAfterRegularHeaders, Http3ErrorCode.ProtocolError);
// block after a regular header field MUST be treated as malformed.
throw new Http3StreamErrorException(CoreStrings.HttpErrorPseudoHeaderFieldAfterRegularHeaders, Http3ErrorCode.MessageError);
}

if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers)
{
// https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1.1.1-3
// Pseudo-header fields MUST NOT appear in trailers.
throw new Http3StreamErrorException(CoreStrings.Http2ErrorTrailersContainPseudoHeaderField, Http3ErrorCode.ProtocolError);
throw new Http3StreamErrorException(CoreStrings.HttpErrorTrailersContainPseudoHeaderField, Http3ErrorCode.MessageError);
}

_requestHeaderParsingState = RequestHeaderParsingState.PseudoHeaderFields;

if (headerField == PseudoHeaderFields.Unknown)
{
// https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1.1.1-3
// Endpoints MUST treat a request or response that contains undefined or invalid pseudo-header
// fields as malformed (Section 8.1.2.6).
throw new Http3StreamErrorException(CoreStrings.Http2ErrorUnknownPseudoHeaderField, Http3ErrorCode.ProtocolError);
// fields as malformed.
throw new Http3StreamErrorException(CoreStrings.HttpErrorUnknownPseudoHeaderField, Http3ErrorCode.MessageError);
}

if (headerField == PseudoHeaderFields.Status)
{
// https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1.1.1-3
// Pseudo-header fields defined for requests MUST NOT appear in responses; pseudo-header fields
// defined for responses MUST NOT appear in requests.
throw new Http3StreamErrorException(CoreStrings.Http2ErrorResponsePseudoHeaderField, Http3ErrorCode.ProtocolError);
throw new Http3StreamErrorException(CoreStrings.HttpErrorResponsePseudoHeaderField, Http3ErrorCode.MessageError);
}

if ((_parsedPseudoHeaderFields & headerField) == headerField)
{
// http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2.3
// All HTTP/2 requests MUST include exactly one valid value for the :method, :scheme, and :path pseudo-header fields
throw new Http3StreamErrorException(CoreStrings.Http2ErrorDuplicatePseudoHeaderField, Http3ErrorCode.ProtocolError);
// https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1.1.1-7
// All HTTP/3 requests MUST include exactly one valid value for the :method, :scheme, and :path pseudo-header fields
throw new Http3StreamErrorException(CoreStrings.HttpErrorDuplicatePseudoHeaderField, Http3ErrorCode.MessageError);
}

if (headerField == PseudoHeaderFields.Method)
Expand All @@ -242,22 +256,22 @@ implementations to these vulnerabilities.*/

if (IsConnectionSpecificHeaderField(name, value))
{
throw new Http3StreamErrorException(CoreStrings.Http2ErrorConnectionSpecificHeaderField, Http3ErrorCode.ProtocolError);
throw new Http3StreamErrorException(CoreStrings.HttpErrorConnectionSpecificHeaderField, Http3ErrorCode.MessageError);
}

// http://httpwg.org/specs/rfc7540.html#rfc.section.8.1.2
// A request or response containing uppercase header field names MUST be treated as malformed (Section 8.1.2.6).
// https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1.1-3
// A request or response containing uppercase header field names MUST be treated as malformed.
for (var i = 0; i < name.Length; i++)
{
if (name[i] >= 65 && name[i] <= 90)
{
if (_requestHeaderParsingState == RequestHeaderParsingState.Trailers)
{
throw new Http3StreamErrorException(CoreStrings.Http2ErrorTrailerNameUppercase, Http3ErrorCode.ProtocolError);
throw new Http3StreamErrorException(CoreStrings.HttpErrorTrailerNameUppercase, Http3ErrorCode.MessageError);
}
else
{
throw new Http3StreamErrorException(CoreStrings.Http2ErrorHeaderNameUppercase, Http3ErrorCode.ProtocolError);
throw new Http3StreamErrorException(CoreStrings.HttpErrorHeaderNameUppercase, Http3ErrorCode.MessageError);
}
}
}
Expand Down Expand Up @@ -521,6 +535,15 @@ private async Task ProcessHeadersFrameAsync<TContext>(IHttpApplication<TContext>
await OnEndStreamReceived();
}

if (!_isMethodConnect && (_parsedPseudoHeaderFields & _mandatoryRequestPseudoHeaderFields) != _mandatoryRequestPseudoHeaderFields)
{
// All HTTP/3 requests MUST include exactly one valid value for the :method, :scheme, and :path pseudo-header
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this not a requirement for HTTP/2?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HTTP/2 and HTTP/3 have the same requirements here.

// fields, unless it is a CONNECT request. An HTTP request that omits mandatory pseudo-header
// fields is malformed.
// https://quicwg.org/base-drafts/draft-ietf-quic-http.html#section-4.1.1.1
throw new Http3StreamErrorException(CoreStrings.HttpErrorMissingMandatoryPseudoHeaderFields, Http3ErrorCode.MessageError);
}

_appCompleted = new TaskCompletionSource();

ThreadPool.UnsafeQueueUserWorkItem(this, preferLocal: false);
Expand Down
Loading