From ee7ff078149a20893e3564e4d42d18c1ea227549 Mon Sep 17 00:00:00 2001 From: Oskar Eichler Date: Fri, 4 Sep 2026 19:57:55 +0700 Subject: [PATCH] Enforce inbound HPACK limits --- lib/http/2/connection.rb | 7 +++++-- lib/http/2/header/decompressor.rb | 16 +++++++++++++++- lib/http/2/header/encoding_context.rb | 13 ++++++++----- sig/header/decompressor.rbs | 4 ++++ sig/header/encoding_context.rbs | 1 + 5 files changed, 33 insertions(+), 8 deletions(-) diff --git a/lib/http/2/connection.rb b/lib/http/2/connection.rb index 450c0eed..a7153e18 100644 --- a/lib/http/2/connection.rb +++ b/lib/http/2/connection.rb @@ -690,11 +690,14 @@ def connection_settings(frame) # Setting header table size might cause some headers evicted case side when :local - @compressor.table_size = v + @decompressor.table_size_limit = v when :remote - @decompressor.table_size = v + @compressor.table_size = v end + when :settings_max_header_list_size + @decompressor.max_header_list_size = v if side == :local + when :settings_enable_push # nothing to do diff --git a/lib/http/2/header/decompressor.rb b/lib/http/2/header/decompressor.rb index 6f0385bb..6ecabdbd 100644 --- a/lib/http/2/header/decompressor.rb +++ b/lib/http/2/header/decompressor.rb @@ -14,10 +14,16 @@ class Decompressor include BufferUtils FORBIDDEN_HEADERS = %w[connection te].freeze + DEFAULT_MAX_HEADER_LIST_SIZE = (2 << 30) - 1 + attr_writer :max_header_list_size - # @param options [Hash] decoding options. Only :table_size is effective. + # @param options [Hash] decoding options. def initialize(options = {}) @cc = EncodingContext.new(options) + @max_header_list_size = options.fetch( + :settings_max_header_list_size, + DEFAULT_MAX_HEADER_LIST_SIZE + ) end # Set dynamic table size in EncodingContext @@ -26,6 +32,10 @@ def table_size=(size) @cc.table_size = size end + def table_size_limit=(size) + @cc.table_size_limit = size + end + # Decodes integer value from provided buffer. # # @param buf [String] @@ -116,6 +126,7 @@ def header(buf) # @return [Array] +[[name, value], ...] def decode(buf, frame = nil) list = [] + header_list_size = 0 decoding_pseudo_headers = true @cc.listen_on_table do until buf.empty? @@ -130,6 +141,9 @@ def decode(buf, frame = nil) decoding_pseudo_headers = is_pseudo_header raise ProtocolError, "invalid header received: #{field}" if FORBIDDEN_HEADERS.include?(field) + header_list_size += field.bytesize + value.bytesize + 32 + raise ProtocolError, "header list exceeds configured maximum" if header_list_size > @max_header_list_size + if frame case field when ":status" diff --git a/lib/http/2/header/encoding_context.rb b/lib/http/2/header/encoding_context.rb index 0dc06c56..2dcdb7ca 100644 --- a/lib/http/2/header/encoding_context.rb +++ b/lib/http/2/header/encoding_context.rb @@ -179,11 +179,9 @@ def process(cmd) when :changetablesize raise CompressionError, "tried to change table size after adding elements to table" if @_table_updated - # we can receive multiple table size change commands inside a header frame. However, - # we should blow up if we receive another frame where the new table size is bigger. - table_size_updated = @limit != @options[:table_size] - - raise CompressionError, "dynamic table size update exceed limit" if !table_size_updated && value > @limit + # Multiple updates may begin a header block, but none may exceed + # the most recently advertised maximum. + raise CompressionError, "dynamic table size update exceeds limit" if value > @options[:table_size] self.table_size = value @@ -313,6 +311,11 @@ def table_size=(size) resize_table(0) end + def table_size_limit=(size) + @options[:table_size] = size + self.table_size = size if @limit > size + end + def listen_on_table yield ensure diff --git a/sig/header/decompressor.rbs b/sig/header/decompressor.rbs index 9f0b473a..0876a9a9 100644 --- a/sig/header/decompressor.rbs +++ b/sig/header/decompressor.rbs @@ -4,10 +4,14 @@ module HTTP2 include BufferUtils FORBIDDEN_HEADERS: Array[String] + DEFAULT_MAX_HEADER_LIST_SIZE: Integer @cc: EncodingContext + @max_header_list_size: Integer def table_size=: (Integer) -> void + def table_size_limit=: (Integer) -> void + def max_header_list_size=: (Integer) -> void def integer: (String, Integer) -> Integer diff --git a/sig/header/encoding_context.rbs b/sig/header/encoding_context.rbs index 481acc5f..933fde66 100644 --- a/sig/header/encoding_context.rbs +++ b/sig/header/encoding_context.rbs @@ -41,6 +41,7 @@ module HTTP2 def addcmd: (String name, String value) -> header_command def table_size=: (Integer) -> void + def table_size_limit=: (Integer) -> void def listen_on_table: { () -> void } -> void