@@ -16,6 +16,7 @@ internal sealed class Http1ChunkedEncodingMessageBody : Http1MessageBody
1616{
1717 // byte consts don't have a data type annotation so we pre-cast it
1818 private const byte ByteCR = ( byte ) '\r ' ;
19+ private const byte ByteLF = ( byte ) '\n ' ;
1920 // "7FFFFFFF\r\n" is the largest chunk size that could be returned as an int.
2021 private const int MaxChunkPrefixBytes = 10 ;
2122
@@ -27,6 +28,8 @@ internal sealed class Http1ChunkedEncodingMessageBody : Http1MessageBody
2728 private readonly Pipe _requestBodyPipe ;
2829 private ReadResult _readResult ;
2930
31+ private static readonly bool InsecureChunkedParsing = AppContext . TryGetSwitch ( "Microsoft.AspNetCore.Server.Kestrel.EnableInsecureChunkedRequestParsing" , out var value ) && value ;
32+
3033 public Http1ChunkedEncodingMessageBody ( Http1Connection context , bool keepAlive )
3134 : base ( context , keepAlive )
3235 {
@@ -345,25 +348,42 @@ private void ParseChunkedPrefix(in ReadOnlySequence<byte> buffer, out SequencePo
345348 KestrelBadHttpRequestException . Throw ( RequestRejectionReason . BadChunkSizeData ) ;
346349 }
347350
351+ // https://www.rfc-editor.org/rfc/rfc9112#section-7.1
352+ // chunk = chunk-size [ chunk-ext ] CRLF
353+ // chunk-data CRLF
354+
355+ // https://www.rfc-editor.org/rfc/rfc9112#section-7.1.1
356+ // chunk-ext = *( BWS ";" BWS chunk-ext-name
357+ // [BWS "=" BWS chunk-ext-val] )
358+ // chunk-ext-name = token
359+ // chunk-ext-val = token / quoted-string
348360 private void ParseExtension ( ReadOnlySequence < byte > buffer , out SequencePosition consumed , out SequencePosition examined )
349361 {
350- // Chunk-extensions not currently parsed
351- // Just drain the data
352- examined = buffer . Start ;
362+ // Chunk-extensions parsed for \r\n and throws for unpaired \r or \n.
353363
354364 do
355365 {
356- SequencePosition ? extensionCursorPosition = buffer . PositionOf ( ByteCR ) ;
366+ SequencePosition ? extensionCursorPosition ;
367+ if ( InsecureChunkedParsing )
368+ {
369+ extensionCursorPosition = buffer . PositionOf ( ByteCR ) ;
370+ }
371+ else
372+ {
373+ extensionCursorPosition = buffer . PositionOfAny ( ByteCR , ByteLF ) ;
374+ }
375+
357376 if ( extensionCursorPosition == null )
358377 {
359378 // End marker not found yet
360379 consumed = buffer . End ;
361380 examined = buffer . End ;
362381 AddAndCheckObservedBytes ( buffer . Length ) ;
363382 return ;
364- } ;
383+ }
365384
366385 var extensionCursor = extensionCursorPosition . Value ;
386+
367387 var charsToByteCRExclusive = buffer . Slice ( 0 , extensionCursor ) . Length ;
368388
369389 var suffixBuffer = buffer . Slice ( extensionCursor ) ;
@@ -378,7 +398,9 @@ private void ParseExtension(ReadOnlySequence<byte> buffer, out SequencePosition
378398 suffixBuffer = suffixBuffer . Slice ( 0 , 2 ) ;
379399 var suffixSpan = suffixBuffer . ToSpan ( ) ;
380400
381- if ( suffixSpan [ 1 ] == '\n ' )
401+ if ( InsecureChunkedParsing
402+ ? ( suffixSpan [ 1 ] == ByteLF )
403+ : ( suffixSpan [ 0 ] == ByteCR && suffixSpan [ 1 ] == ByteLF ) )
382404 {
383405 // We consumed the \r\n at the end of the extension, so switch modes.
384406 _mode = _inputLength > 0 ? Mode . Data : Mode . Trailer ;
@@ -387,13 +409,22 @@ private void ParseExtension(ReadOnlySequence<byte> buffer, out SequencePosition
387409 examined = suffixBuffer . End ;
388410 AddAndCheckObservedBytes ( charsToByteCRExclusive + 2 ) ;
389411 }
390- else
412+ else if ( InsecureChunkedParsing )
391413 {
414+ examined = buffer . Start ;
392415 // Don't consume suffixSpan[1] in case it is also a \r.
393416 buffer = buffer . Slice ( charsToByteCRExclusive + 1 ) ;
394417 consumed = extensionCursor ;
395418 AddAndCheckObservedBytes ( charsToByteCRExclusive + 1 ) ;
396419 }
420+ else
421+ {
422+ consumed = suffixBuffer . End ;
423+ examined = suffixBuffer . End ;
424+
425+ // We have \rX or \nX, that's an invalid extension.
426+ KestrelBadHttpRequestException . Throw ( RequestRejectionReason . BadChunkExtension ) ;
427+ }
397428 } while ( _mode == Mode . Extension ) ;
398429 }
399430
0 commit comments