Skip to content

Commit 84bdd8d

Browse files
net.http: h2 server — reject NUL/CR/LF octets in request field values
RFC 9113 §8.2.1: a field value MUST NOT contain NUL (0x00), LF (0x0a), or CR (0x0d). The new request validation checked field names but not values, so a value with an embedded CR/LF was accepted and passed through into req.header — a malformed request and, if the request were forwarded to an HTTP/1.x peer, a header-injection / request-smuggling vector. Reject such values (and the same octets in pseudo-header values) as a stream error. Found by a requirements-driven review pass (RFC §8.2.1) — not covered by the h2spec suite. Adds a unit test (CR/LF in a field value -> RST_STREAM). Co-Authored-By: WOZCODE <contact@withwoz.com>
1 parent 51c8732 commit 84bdd8d

2 files changed

Lines changed: 30 additions & 2 deletions

File tree

vlib/net/http/h2_server.v

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,17 +43,34 @@ fn h2_all_digits(s string) bool {
4343
return true
4444
}
4545

46+
// h2_field_value_has_forbidden_octet reports whether a field value contains an
47+
// octet RFC 9113 §8.2.1 forbids: NUL (0x00), LF (0x0a), or CR (0x0d). Such a
48+
// value makes the request malformed and, if the request were forwarded to an
49+
// HTTP/1.x peer, would be a header-injection / request-smuggling vector.
50+
fn h2_field_value_has_forbidden_octet(value string) bool {
51+
for ch in value {
52+
if ch == 0 || ch == 0x0a || ch == 0x0d {
53+
return true
54+
}
55+
}
56+
return false
57+
}
58+
4659
// h2_request_field_error returns a non-empty reason when a regular (non-pseudo)
4760
// request header field is malformed per RFC 9113 §8.2: names must be lowercase
48-
// and non-empty, connection-specific fields are forbidden, and TE may only carry
49-
// the value "trailers". An empty return means the field is valid.
61+
// and non-empty, values must not contain NUL/CR/LF, connection-specific fields
62+
// are forbidden, and TE may only carry the value "trailers". An empty return
63+
// means the field is valid.
5064
fn h2_request_field_error(name string, value string) string {
5165
if name.len == 0 {
5266
return 'empty header field name'
5367
}
5468
if name != name.to_lower() {
5569
return 'uppercase header field name "${name}"'
5670
}
71+
if h2_field_value_has_forbidden_octet(value) {
72+
return 'forbidden NUL/CR/LF octet in value of "${name}"'
73+
}
5774
if name in h2_conn_specific_headers {
5875
return 'connection-specific header field "${name}"'
5976
}
@@ -79,6 +96,9 @@ fn h2_validate_request_pseudo(headers []H2HeaderField) ! {
7996
if seen_regular {
8097
return error('pseudo-header "${f.name}" after a regular field')
8198
}
99+
if h2_field_value_has_forbidden_octet(f.value) {
100+
return error('forbidden NUL/CR/LF octet in pseudo-header "${f.name}"')
101+
}
82102
match f.name {
83103
':method' {
84104
if has_method {

vlib/net/http/h2_server_test.v

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,14 @@ fn test_h2_server_rejects_duplicate_pseudo() {
433433
}
434434
}
435435

436+
// RFC 9113 §8.2.1: a field value containing NUL/CR/LF is malformed (and would be
437+
// a header-injection vector if forwarded to an HTTP/1.x peer).
438+
fn test_h2_server_rejects_control_char_in_value() {
439+
mut fields := valid_get_pseudo.clone()
440+
fields << H2HeaderField{'x-evil', 'a\r\nInjected: 1'}
441+
assert_request_malformed(fields, 'CR/LF in field value')
442+
}
443+
436444
// RFC 9113 §8.3.1: an empty :path pseudo-header is malformed for http/https.
437445
fn test_h2_server_rejects_empty_path() {
438446
fields := [

0 commit comments

Comments
 (0)