From 5377e575dfad790240e9b4fc64c54f1d7fda1713 Mon Sep 17 00:00:00 2001 From: Oskar Eichler Date: Fri, 28 Aug 2026 03:13:59 +0300 Subject: [PATCH 1/4] Parse extension headers with HTTP whitespace and full boundaries --- lib/websocket/extensions/parser.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/websocket/extensions/parser.rb b/lib/websocket/extensions/parser.rb index f8d8891..354a29a 100644 --- a/lib/websocket/extensions/parser.rb +++ b/lib/websocket/extensions/parser.rb @@ -7,10 +7,10 @@ class Parser TOKEN = /([!#\$%&'\*\+\-\.\^_`\|~0-9A-Za-z]+)/ NOTOKEN = /([^!#\$%&'\*\+\-\.\^_`\|~0-9A-Za-z])/ QUOTED = /"((?:\\[\x00-\x7f]|[^\x00-\x08\x0a-\x1f\x7f"\\])*)"/ - PARAM = %r{#{ TOKEN.source }(?:=(?:#{ TOKEN.source }|#{ QUOTED.source }))?} - EXT = %r{#{ TOKEN.source }(?: *; *#{ PARAM.source })*} - EXT_LIST = %r{^#{ EXT.source }(?: *, *#{ EXT.source })*$} - NUMBER = /^-?(0|[1-9][0-9]*)(\.[0-9]+)?$/ + PARAM = %r{#{ TOKEN.source }(?:[ \t]*=[ \t]*(?:#{ TOKEN.source }|#{ QUOTED.source }))?} + EXT = %r{#{ TOKEN.source }(?:[ \t]*;[ \t]*#{ PARAM.source })*} + EXT_LIST = %r{\A[ \t]*#{ EXT.source }(?:[ \t]*,[ \t]*#{ EXT.source })*[ \t]*\z} + NUMBER = /\A-?(0|[1-9][0-9]*)(\.[0-9]+)?\z/ ParseError = Class.new(ArgumentError) @@ -23,6 +23,7 @@ def self.parse_header(header) end scanner = StringScanner.new(header) + scanner.scan(/[ \t]*/) value = scanner.scan(EXT) until value.nil? @@ -51,7 +52,7 @@ def self.parse_header(header) offers.push(name, offer) - scanner.scan(/ *, */) + scanner.scan(/[ \t]*,[ \t]*/) value = scanner.scan(EXT) end offers From 8874ee8c183b35cd8dee6d13ae60778b75d07383 Mon Sep 17 00:00:00 2001 From: Oskar Eichler Date: Fri, 28 Aug 2026 03:16:45 +0300 Subject: [PATCH 2/4] Skip separator bytes without allocating strings --- lib/websocket/extensions/parser.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/websocket/extensions/parser.rb b/lib/websocket/extensions/parser.rb index 354a29a..1939658 100644 --- a/lib/websocket/extensions/parser.rb +++ b/lib/websocket/extensions/parser.rb @@ -23,7 +23,7 @@ def self.parse_header(header) end scanner = StringScanner.new(header) - scanner.scan(/[ \t]*/) + scanner.skip(/[ \t]*/) value = scanner.scan(EXT) until value.nil? @@ -52,7 +52,7 @@ def self.parse_header(header) offers.push(name, offer) - scanner.scan(/[ \t]*,[ \t]*/) + scanner.skip(/[ \t]*,[ \t]*/) value = scanner.scan(EXT) end offers From e6db210110e93e1fa436210dd5401df9e5e50c56 Mon Sep 17 00:00:00 2001 From: Oskar Eichler Date: Fri, 28 Aug 2026 03:19:14 +0300 Subject: [PATCH 3/4] Combine leading whitespace with the first extension scan --- lib/websocket/extensions/parser.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/websocket/extensions/parser.rb b/lib/websocket/extensions/parser.rb index 1939658..ad65793 100644 --- a/lib/websocket/extensions/parser.rb +++ b/lib/websocket/extensions/parser.rb @@ -9,6 +9,7 @@ class Parser QUOTED = /"((?:\\[\x00-\x7f]|[^\x00-\x08\x0a-\x1f\x7f"\\])*)"/ PARAM = %r{#{ TOKEN.source }(?:[ \t]*=[ \t]*(?:#{ TOKEN.source }|#{ QUOTED.source }))?} EXT = %r{#{ TOKEN.source }(?:[ \t]*;[ \t]*#{ PARAM.source })*} + EXT_START = %r{[ \t]*#{ EXT.source }} EXT_LIST = %r{\A[ \t]*#{ EXT.source }(?:[ \t]*,[ \t]*#{ EXT.source })*[ \t]*\z} NUMBER = /\A-?(0|[1-9][0-9]*)(\.[0-9]+)?\z/ @@ -23,8 +24,7 @@ def self.parse_header(header) end scanner = StringScanner.new(header) - scanner.skip(/[ \t]*/) - value = scanner.scan(EXT) + value = scanner.scan(EXT_START) until value.nil? params = value.scan(PARAM) From 4440d94b5d6584921c40beb933122048ef5dfef8 Mon Sep 17 00:00:00 2001 From: Oskar Eichler <62393985+OskarEichler@users.noreply.github.com> Date: Fri, 28 Aug 2026 14:43:04 +0300 Subject: [PATCH 4/4] Add parser regression coverage --- spec/websocket/extensions/parser_spec.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/spec/websocket/extensions/parser_spec.rb b/spec/websocket/extensions/parser_spec.rb index 7458210..61904fa 100644 --- a/spec/websocket/extensions/parser_spec.rb +++ b/spec/websocket/extensions/parser_spec.rb @@ -24,6 +24,18 @@ def parse(string) ] end + it "parses HTTP whitespace around delimiters" do + expect(parse " \ta \t; mode \t= compress \t, other \t").to eq [ + { :name => "a", :params => { "mode" => "compress" } }, + { :name => "other", :params => {} } + ] + end + + it "rejects input outside the complete header value" do + expect { parse "\na" }.to raise_error(WebSocket::Extensions::Parser::ParseError) + expect { parse "a\n" }.to raise_error(WebSocket::Extensions::Parser::ParseError) + end + it "parses two offers with no params" do expect(parse 'a, b').to eq [ { :name => "a", :params => {} }, { :name => "b", :params => {} }