Skip to content

Commit

Permalink
HTTP/3: Header limits and validation (#31928)
Browse files Browse the repository at this point in the history
  • Loading branch information
JamesNK authored Apr 19, 2021
1 parent ea2f559 commit 8d8deb7
Show file tree
Hide file tree
Showing 8 changed files with 351 additions and 57 deletions.
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
// 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

0 comments on commit 8d8deb7

Please sign in to comment.