Skip to content
Merged
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ jobs:
linux-compat:
runs-on: ubuntu-24.04 # latest
strategy:
fail-fast: false
Comment thread
azkrishpy marked this conversation as resolved.
matrix:
image:
- manylinux1-x64
Expand All @@ -47,6 +48,7 @@ jobs:
linux-compiler-compat:
runs-on: ubuntu-24.04 # latest
strategy:
fail-fast: false
matrix:
compiler:
- name: clang-6
Expand Down Expand Up @@ -95,6 +97,7 @@ jobs:
clang-sanitizers:
runs-on: ubuntu-24.04 # latest
strategy:
fail-fast: false
matrix:
sanitizers: [",thread", ",address,undefined"]
steps:
Expand Down
8 changes: 8 additions & 0 deletions include/aws/http/private/hpack.h
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ struct aws_hpack_decoder {
/* SETTINGS_HEADER_TABLE_SIZE from http2 */
size_t dynamic_table_protocol_max_size_setting;

/* Maximum decoded header-list size (from SETTINGS_MAX_HEADER_LIST_SIZE).
* Individual HPACK string lengths are checked against this to prevent unbounded growth. */
uint64_t max_header_list_size;

/* PRO TIP: Don't union progress_integer and progress_string together, since string_decode calls integer_decode */
struct hpack_progress_integer {
enum {
Expand Down Expand Up @@ -252,6 +256,10 @@ void aws_hpack_decoder_clean_up(struct aws_hpack_decoder *decoder);
AWS_HTTP_API
void aws_hpack_decoder_update_max_table_size(struct aws_hpack_decoder *decoder, uint32_t new_max_size);

/* Update the maximum header-list size used to bound individual HPACK string lengths. */
AWS_HTTP_API
void aws_hpack_decoder_set_max_header_list_size(struct aws_hpack_decoder *decoder, uint64_t max_header_list_size);

/**
* Decode the next entry in the header-block-fragment.
* If result->type is ONGOING, then call decode() again with more data to resume decoding.
Expand Down
1 change: 1 addition & 0 deletions source/h2_decoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -1615,4 +1615,5 @@ void aws_h2_decoder_set_setting_max_frame_size(struct aws_h2_decoder *decoder, u

void aws_h2_decoder_set_setting_max_header_list_size(struct aws_h2_decoder *decoder, uint32_t data) {
decoder->settings.max_header_list_size = data;
aws_hpack_decoder_set_max_header_list_size(&decoder->hpack, data);
}
18 changes: 14 additions & 4 deletions source/hpack_decoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0.
*/
#include <aws/http/private/h2_frames.h>
#include <aws/http/private/hpack.h>

#define HPACK_LOGF(level, decoder, text, ...) \
Expand All @@ -25,6 +26,10 @@ void aws_hpack_decoder_init(struct aws_hpack_decoder *decoder, struct aws_alloca
aws_byte_buf_init(&decoder->progress_entry.scratch, allocator, s_hpack_decoder_scratch_initial_size);

decoder->dynamic_table_protocol_max_size_setting = aws_hpack_get_dynamic_table_max_size(&decoder->context);

/* Default to the initial SETTINGS_MAX_HEADER_LIST_SIZE value.
* h2_decoder updates this if the setting changes via SETTINGS frame. */
decoder->max_header_list_size = aws_h2_settings_initial[AWS_HTTP2_SETTINGS_MAX_HEADER_LIST_SIZE];
}

void aws_hpack_decoder_clean_up(struct aws_hpack_decoder *decoder) {
Expand All @@ -33,6 +38,10 @@ void aws_hpack_decoder_clean_up(struct aws_hpack_decoder *decoder) {
AWS_ZERO_STRUCT(*decoder);
}

void aws_hpack_decoder_set_max_header_list_size(struct aws_hpack_decoder *decoder, uint64_t max_header_list_size) {
decoder->max_header_list_size = max_header_list_size;
}

static const struct aws_http_header *s_get_header_u64(const struct aws_hpack_decoder *decoder, uint64_t index) {
if (index > SIZE_MAX) {
HPACK_LOG(ERROR, decoder, "Header index is absurdly large");
Expand Down Expand Up @@ -167,8 +176,11 @@ int aws_hpack_decode_string(
goto handle_complete;
}

if (progress->length > SIZE_MAX) {
return aws_raise_error(AWS_ERROR_OVERFLOW_DETECTED);
/* Reject if the declared string length alone exceeds the header-list budget.
* A single string cannot be larger than the entire allowed header-list. */
if (progress->length > decoder->max_header_list_size) {
HPACK_LOG(ERROR, decoder, "HPACK string length exceeds SETTINGS_MAX_HEADER_LIST_SIZE");
return aws_raise_error(AWS_ERROR_HTTP_PROTOCOL_ERROR);
}

progress->state = HPACK_STRING_STATE_VALUE;
Expand Down Expand Up @@ -205,8 +217,6 @@ int aws_hpack_decode_string(
* "A padding not corresponding to the most significant bits of the
* code for the EOS symbol MUST be treated as a decoding error" */

/* #TODO impose limits on string length */

goto handle_complete;
}
} break;
Expand Down
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ add_h2_decoder_test_set(h2_decoder_preface_from_client)
add_h2_decoder_test_set(h2_decoder_err_bad_preface_from_client_1)
add_h2_decoder_test_set(h2_decoder_err_bad_preface_from_client_2)
add_h2_decoder_test_set(h2_decoder_err_bad_preface_from_client_3)
add_h2_decoder_test_set(h2_decoder_err_hpack_string_declared_length_exceeds_limit)

add_test_case(h2_client_sanity_check)
add_test_case(h2_client_stream_create)
Expand Down
22 changes: 22 additions & 0 deletions tests/fuzz/fuzz_h2_decoder_correct.c
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,21 @@ static struct aws_http_headers *s_generate_headers(
struct aws_byte_buf buf;
aws_byte_buf_init(&buf, allocator, 1024);

/* Track cumulative header-list size (RFC-9113 6.5.2: name.len + value.len + 32 per field).
* Stop generating headers before we exceed SETTINGS_MAX_HEADER_LIST_SIZE,
* since the decoder rightfully rejects oversized header lists.
* Start with the size consumed by pseudo-headers already added above. */
uint64_t header_list_size = 0;
const uint64_t max_header_list_size = aws_h2_settings_initial[AWS_HTTP2_SETTINGS_MAX_HEADER_LIST_SIZE];

/* Account for pseudo-headers already added */
size_t num_pseudo_headers = aws_http_headers_count(headers);
for (size_t i = 0; i < num_pseudo_headers; i++) {
struct aws_http_header pseudo;
aws_http_headers_get_index(headers, i, &pseudo);
header_list_size += pseudo.name.len + pseudo.value.len + 32;
}

while (input->len) {
buf.len = 0;

Expand Down Expand Up @@ -105,6 +120,13 @@ static struct aws_http_headers *s_generate_headers(
header.value = aws_byte_cursor_from_buf(&buf);
aws_byte_cursor_advance(&header.value, header.name.len);

/* Stop if adding this header would exceed the header-list budget */
uint64_t field_size = header.name.len + header.value.len + 32;
if (header_list_size + field_size > max_header_list_size) {
break;
}
header_list_size += field_size;

aws_http_headers_add_header(headers, &header);
}

Expand Down
56 changes: 56 additions & 0 deletions tests/test_h2_decoder.c
Original file line number Diff line number Diff line change
Expand Up @@ -2980,3 +2980,59 @@ H2_DECODER_ON_SERVER_PREFACE_TEST(h2_decoder_err_bad_preface_from_client_3) {

return AWS_OP_SUCCESS;
}

/* Verify that an HPACK string whose declared length exceeds SETTINGS_MAX_HEADER_LIST_SIZE
* (default from h2 settings array) is rejected immediately, before the string data arrives. */
H2_DECODER_ON_CLIENT_TEST(h2_decoder_err_hpack_string_declared_length_exceeds_limit) {
(void)allocator;
struct fixture *fixture = ctx;

/* Use one byte over the default header-list size limit as our declared string length */
const uint64_t max_header_list_size = aws_h2_settings_initial[AWS_HTTP2_SETTINGS_MAX_HEADER_LIST_SIZE];
const uint64_t string_length = max_header_list_size + 1;

/* Encode string_length as an HPACK 7-bit prefix integer (H=0, no Huffman).
* Format: 0x7f (prefix full), then (string_length - 127) as continuation bytes. */
uint8_t varint[8];
size_t varint_len = 0;
varint[varint_len++] = 0x7f;
uint64_t remainder = string_length - 127;
while (remainder >= 128) {
varint[varint_len++] = (uint8_t)((remainder & 0x7f) | 0x80);
remainder >>= 7;
}
varint[varint_len++] = (uint8_t)(remainder & 0x7f);

/* Build: 9-byte frame header + 1-byte literal prefix + varint + 3 bytes of name data */
const size_t payload_len = 1 + varint_len + 3;
struct aws_byte_buf input;
aws_byte_buf_init(&input, allocator, 9 + payload_len);

/* HTTP/2 frame header */
uint8_t frame_header[] = {
(uint8_t)((payload_len >> 16) & 0xFF),
(uint8_t)((payload_len >> 8) & 0xFF),
(uint8_t)(payload_len & 0xFF), /* Length (24) */
AWS_H2_FRAME_T_HEADERS, /* Type */
0x00, /* Flags: no END_HEADERS */
0x00,
0x00,
0x00,
0x01, /* Stream ID: 1 */
};
aws_byte_buf_write(&input, frame_header, sizeof(frame_header));

/* HPACK payload: literal header field (new name, never indexed) + string length + partial data */
uint8_t literal_prefix = 0x00;
aws_byte_buf_write_u8(&input, literal_prefix);
aws_byte_buf_write(&input, varint, varint_len);
uint8_t name_data[] = {'A', 'A', 'A'};
aws_byte_buf_write(&input, name_data, sizeof(name_data));

/* The decoder must reject because the declared string length exceeds the header-list budget */
struct aws_h2err err = s_decode_all(fixture, aws_byte_cursor_from_buf(&input));
ASSERT_TRUE(aws_h2err_failed(err));

aws_byte_buf_clean_up(&input);
return AWS_OP_SUCCESS;
}
Loading