diff --git a/package-lock.json b/package-lock.json index 443f152d..94e32de0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "llhttp", - "version": "2.2.1", + "version": "2.3.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 01e0db7d..f94ae6e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "llhttp", - "version": "2.2.1", + "version": "2.3.0", "description": "HTTP parser in LLVM IR", "main": "lib/llhttp.js", "types": "lib/llhttp.d.ts", diff --git a/src/llhttp/http.ts b/src/llhttp/http.ts index cd0ed332..1cfa1f61 100644 --- a/src/llhttp/http.ts +++ b/src/llhttp/http.ts @@ -46,6 +46,7 @@ const NODES: ReadonlyArray = [ 'header_field_start', 'header_field', 'header_field_colon', + 'header_field_colon_discard_ws', 'header_field_general', 'header_field_general_otherwise', 'header_value_discard_ws', @@ -372,13 +373,32 @@ export class HTTP { .select(SPECIAL_HEADERS, this.store('header_state', 'header_field_colon')) .otherwise(this.resetHeaderState('header_field_general')); + const onInvalidHeaderFieldChar = + p.error(ERROR.INVALID_HEADER_TOKEN, 'Invalid header field char'); + + const checkLenientFlagsOnColon = + this.testFlags(FLAGS.LENIENT, { + 1: n('header_field_colon_discard_ws'), + }, span.headerField.end().skipTo(onInvalidHeaderFieldChar)); + n('header_field_colon') - .match(' ', n('header_field_colon')) + // https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.4 + // Whitespace character is not allowed between the header field-name + // and colon. If the next token matches whitespace then throw an error. + // + // Add a check for the lenient flag. If the lenient flag is set, the + // whitespace token is allowed to support legacy code not following + // http specs. + .peek(' ', checkLenientFlagsOnColon) .peek(':', span.headerField.end().skipTo(n('header_value_discard_ws'))) // Fallback to general header, there're additional characters: // `Connection-Duration` instead of `Connection` and so on. .otherwise(this.resetHeaderState('header_field_general')); + n('header_field_colon_discard_ws') + .match(' ', n('header_field_colon_discard_ws')) + .otherwise(n('header_field_colon')); + n('header_field_general') .match(this.TOKEN, n('header_field_general')) .otherwise(n('header_field_general_otherwise')); @@ -702,10 +722,11 @@ export class HTTP { .otherwise(p.error(ERROR.INVALID_CHUNK_SIZE, 'Invalid character in chunk size')); - // just ignore this. n('chunk_parameters') .match('\r', n('chunk_size_almost_done')) - .skipTo(n('chunk_parameters')); + .match(HEADER_CHARS, n('chunk_parameters')) + .otherwise(p.error(ERROR.STRICT, + 'Invalid character in chunk parameters')); if (this.mode === 'strict') { n('chunk_size_almost_done') diff --git a/test/request/connection.md b/test/request/connection.md index f6e4007e..f2b69c7a 100644 --- a/test/request/connection.md +++ b/test/request/connection.md @@ -348,6 +348,52 @@ off=78 message complete off=78 error code=22 reason="Pause on CONNECT/Upgrade" ``` +### Invalid whitespace token with `Connection` header field + + +```http +PUT /url HTTP/1.1 +Connection : upgrade +Content-Length: 4 +Upgrade: ws + +abcdefgh +``` + +```log +off=0 message begin +off=4 len=4 span[url]="/url" +off=19 len=10 span[header_field]="Connection" +off=30 error code=10 reason="Invalid header field char" +``` + +### Invalid whitespace token with `Connection` header field (lenient) + + +```http +PUT /url HTTP/1.1 +Connection : upgrade +Content-Length: 4 +Upgrade: ws + +abcdefgh +``` + +```log +off=0 message begin +off=4 len=4 span[url]="/url" +off=19 len=11 span[header_field]="Connection " +off=32 len=7 span[header_value]="upgrade" +off=41 len=14 span[header_field]="Content-Length" +off=57 len=1 span[header_value]="4" +off=60 len=7 span[header_field]="Upgrade" +off=69 len=2 span[header_value]="ws" +off=75 headers complete method=4 v=1/1 flags=134 content_length=4 +off=75 len=4 span[body]="abcd" +off=79 message complete +off=79 error code=22 reason="Pause on CONNECT/Upgrade" +``` + ## `upgrade` ### Setting a flag and pausing diff --git a/test/request/content-length.md b/test/request/content-length.md index 80c2f646..a4cad060 100644 --- a/test/request/content-length.md +++ b/test/request/content-length.md @@ -151,6 +151,54 @@ off=57 len=8 span[header_value]="identity" off=69 headers complete method=4 v=1/1 flags=320 content_length=1 ``` +## Invalid whitespace token with `Content-Length` header field + + +```http +PUT /url HTTP/1.1 +Connection: upgrade +Content-Length : 4 +Upgrade: ws + +abcdefgh +``` + +```log +off=0 message begin +off=4 len=4 span[url]="/url" +off=19 len=10 span[header_field]="Connection" +off=31 len=7 span[header_value]="upgrade" +off=40 len=14 span[header_field]="Content-Length" +off=55 error code=10 reason="Invalid header field char" +``` + +## Invalid whitespace token with `Content-Length` header field (lenient) + + +```http +PUT /url HTTP/1.1 +Connection: upgrade +Content-Length : 4 +Upgrade: ws + +abcdefgh +``` + +```log +off=0 message begin +off=4 len=4 span[url]="/url" +off=19 len=10 span[header_field]="Connection" +off=31 len=7 span[header_value]="upgrade" +off=40 len=15 span[header_field]="Content-Length " +off=57 len=1 span[header_value]="4" +off=60 len=7 span[header_field]="Upgrade" +off=69 len=2 span[header_value]="ws" +off=75 headers complete method=4 v=1/1 flags=134 content_length=4 +off=75 len=4 span[body]="abcd" +off=79 message complete +off=79 error code=22 reason="Pause on CONNECT/Upgrade" +``` + ## Funky `Content-Length` with body diff --git a/test/request/transfer-encoding.md b/test/request/transfer-encoding.md index 9b81ef9d..6c0e6f38 100644 --- a/test/request/transfer-encoding.md +++ b/test/request/transfer-encoding.md @@ -411,3 +411,25 @@ off=52 len=3 span[body]="foo" off=57 chunk complete off=57 error code=12 reason="Invalid character in chunk size" ``` + +## Validate chunk parameters + + +```http +PUT /url HTTP/1.1 +Transfer-Encoding: chunked + +3 \n \r\n\ +foo + + +``` + +```log +off=0 message begin +off=4 len=4 span[url]="/url" +off=19 len=17 span[header_field]="Transfer-Encoding" +off=38 len=7 span[header_value]="chunked" +off=49 headers complete method=4 v=1/1 flags=208 content_length=0 +off=51 error code=2 reason="Invalid character in chunk parameters" +```