Skip to content

Commit eaf2e8b

Browse files
haproxy: add http harness (#15287)
Signed-off-by: David Korczynski <david@adalogics.com>
1 parent c138f55 commit eaf2e8b

File tree

2 files changed

+315
-1
lines changed

2 files changed

+315
-1
lines changed

projects/haproxy/build.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ SETTINGS="-Iinclude -g -DUSE_POLL -DUSE_TPROXY -DCONFIG_HAPROXY_VERSION=\"\" -DC
3434
$CC $CFLAGS $SETTINGS -c -o ./src/haproxy.o ./src/haproxy.c
3535
ar cr libhaproxy.a ./src/*.o
3636

37-
for fuzzer in hpack_decode cfg_parser h1_parse h1_htx; do
37+
for fuzzer in hpack_decode cfg_parser h1_parse h1_htx http; do
3838
cp $SRC/fuzz_${fuzzer}.c .
3939
$CC $CFLAGS $SETTINGS -c fuzz_${fuzzer}.c -o fuzz_${fuzzer}.o
4040
$CXX -g $CXXFLAGS $LIB_FUZZING_ENGINE fuzz_${fuzzer}.o libhaproxy.a -o $OUT/fuzz_${fuzzer}

projects/haproxy/fuzz_http.c

Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/*
18+
* Fuzzer for HAProxy's HTTP utility functions in src/http.c.
19+
*
20+
* This targets the core HTTP semantic parsing functions that process
21+
* untrusted input from clients and backends:
22+
*
23+
* - URI parsing: http_parse_scheme, http_parse_authority, http_parse_path
24+
* - Content-Length: http_parse_cont_len_header (request smuggling surface)
25+
* - Cookie parsing: http_extract_cookie_value, http_extract_next_cookie_name
26+
* - Header utilities: http_header_match2, http_find_hdr_value_end
27+
* - HTTP line parsing: http_parse_header, http_parse_stline,
28+
* http_parse_status_val
29+
* - Quality values: http_parse_qvalue
30+
* - ETag comparison: http_compare_etags
31+
* - Host/port: http_get_host_port, http_is_default_port
32+
* - Method lookup: find_http_meth
33+
* - Scheme validation: http_validate_scheme
34+
* - URL parameters: http_find_next_url_param
35+
*
36+
* The first byte of fuzz input selects which function group to exercise,
37+
* maximizing coverage across the many independent parsers.
38+
*/
39+
40+
#include <haproxy/http.h>
41+
#include <haproxy/http-hdr.h>
42+
#include <haproxy/global.h>
43+
44+
#include <stdint.h>
45+
#include <string.h>
46+
#include <stdlib.h>
47+
48+
/* Consume one byte from the fuzz data to use as a selector. */
49+
static inline uint8_t fuzz_consume_byte(const uint8_t **data, size_t *size) {
50+
if (*size == 0)
51+
return 0;
52+
uint8_t b = **data;
53+
(*data)++;
54+
(*size)--;
55+
return b;
56+
}
57+
58+
/* Exercise URI parsing: scheme, authority, path */
59+
static void fuzz_uri_parsing(const uint8_t *data, size_t size) {
60+
char *buf = (char *)malloc(size + 1);
61+
if (!buf)
62+
return;
63+
memcpy(buf, data, size);
64+
buf[size] = '\0';
65+
66+
struct ist uri = ist2(buf, size);
67+
struct http_uri_parser parser = http_uri_parser_init(uri);
68+
69+
http_parse_scheme(&parser);
70+
http_parse_authority(&parser, 1);
71+
http_parse_path(&parser);
72+
73+
/* Also test with no_userinfo=0 */
74+
parser = http_uri_parser_init(uri);
75+
http_parse_scheme(&parser);
76+
http_parse_authority(&parser, 0);
77+
78+
free(buf);
79+
}
80+
81+
/* Exercise Content-Length header parsing — critical for request smuggling */
82+
static void fuzz_content_length(const uint8_t *data, size_t size) {
83+
char *buf = (char *)malloc(size + 1);
84+
if (!buf)
85+
return;
86+
memcpy(buf, data, size);
87+
buf[size] = '\0';
88+
89+
struct ist value = ist2(buf, size);
90+
unsigned long long body_len = 0;
91+
92+
/* First occurrence */
93+
int ret = http_parse_cont_len_header(&value, &body_len, 0);
94+
95+
/* Simulate a second header with same data (duplicate CL detection) */
96+
if (ret > 0) {
97+
struct ist value2 = ist2(buf, size);
98+
http_parse_cont_len_header(&value2, &body_len, 1);
99+
}
100+
101+
free(buf);
102+
}
103+
104+
/* Exercise cookie extraction */
105+
static void fuzz_cookie_parsing(const uint8_t *data, size_t size) {
106+
if (size < 2)
107+
return;
108+
109+
char *buf = (char *)malloc(size + 1);
110+
if (!buf)
111+
return;
112+
memcpy(buf, data, size);
113+
buf[size] = '\0';
114+
115+
char *hdr = buf;
116+
const char *hdr_end = buf + size;
117+
char *value = NULL;
118+
size_t value_l = 0;
119+
120+
/* Extract cookie with empty name (match any) */
121+
http_extract_cookie_value(hdr, hdr_end, "", 0, 1, &value, &value_l);
122+
123+
/* Extract cookie with a specific 4-byte name from start of input */
124+
if (size > 6) {
125+
char name[5];
126+
memcpy(name, data, 4);
127+
name[4] = '\0';
128+
char *search_start = buf + 4;
129+
http_extract_cookie_value(search_start, hdr_end, name, 4, 1,
130+
&value, &value_l);
131+
}
132+
133+
/* Test http_extract_next_cookie_name */
134+
char *ptr = NULL;
135+
size_t len = 0;
136+
http_extract_next_cookie_name(hdr, buf + size, 1, &ptr, &len);
137+
http_extract_next_cookie_name(hdr, buf + size, 0, &ptr, &len);
138+
139+
free(buf);
140+
}
141+
142+
/* Exercise header/line parsing utilities */
143+
static void fuzz_header_parsing(const uint8_t *data, size_t size) {
144+
char *buf = (char *)malloc(size + 1);
145+
if (!buf)
146+
return;
147+
memcpy(buf, data, size);
148+
buf[size] = '\0';
149+
150+
struct ist hdr = ist2(buf, size);
151+
struct ist name, value;
152+
153+
/* Parse a header line */
154+
http_parse_header(hdr, &name, &value);
155+
156+
/* Parse a start line */
157+
struct ist p1, p2, p3;
158+
http_parse_stline(hdr, &p1, &p2, &p3);
159+
160+
/* Parse status value */
161+
struct ist status, reason;
162+
http_parse_status_val(hdr, &status, &reason);
163+
164+
/* header_match2 with a known header name */
165+
http_header_match2(buf, buf + size, "content-type", 12);
166+
http_header_match2(buf, buf + size, "host", 4);
167+
168+
/* find_hdr_value_end */
169+
http_find_hdr_value_end(buf, buf + size);
170+
171+
free(buf);
172+
}
173+
174+
/* Exercise host/port, scheme validation, method lookup, qvalue, etags */
175+
static void fuzz_misc(const uint8_t *data, size_t size) {
176+
char *buf = (char *)malloc(size + 1);
177+
if (!buf)
178+
return;
179+
memcpy(buf, data, size);
180+
buf[size] = '\0';
181+
182+
struct ist s = ist2(buf, size);
183+
184+
/* Host port extraction */
185+
http_get_host_port(s);
186+
187+
/* Default port check */
188+
http_is_default_port(IST_NULL, s);
189+
http_is_default_port(ist("http://"), s);
190+
http_is_default_port(ist("https://"), s);
191+
192+
/* Scheme validation */
193+
http_validate_scheme(s);
194+
195+
/* Method lookup */
196+
find_http_meth(buf, size);
197+
198+
/* Status index */
199+
if (size >= 2) {
200+
unsigned int status = (data[0] << 8) | data[1];
201+
http_get_status_idx(status);
202+
http_get_reason(status);
203+
}
204+
205+
/* qvalue parsing */
206+
const char *end = NULL;
207+
http_parse_qvalue(buf, &end);
208+
209+
/* ETag comparison: split input in half */
210+
if (size >= 4) {
211+
size_t half = size / 2;
212+
struct ist etag1 = ist2(buf, half);
213+
struct ist etag2 = ist2(buf + half, size - half);
214+
http_compare_etags(etag1, etag2);
215+
}
216+
217+
/* Trim leading spaces (safe with standalone buffers) */
218+
http_trim_leading_spht(s);
219+
/* NOTE: http_trim_trailing_spht is intentionally not fuzzed here because
220+
* it reads ret.ptr[-1] which assumes the ist points into the middle of
221+
* a larger buffer. Calling it with a standalone allocation would cause
222+
* a false-positive heap-buffer-overflow. */
223+
224+
free(buf);
225+
}
226+
227+
/* Exercise URL parameter finding */
228+
static void fuzz_url_params(const uint8_t *data, size_t size) {
229+
if (size < 4)
230+
return;
231+
232+
char *buf = (char *)malloc(size + 1);
233+
if (!buf)
234+
return;
235+
memcpy(buf, data, size);
236+
buf[size] = '\0';
237+
238+
/* Simple single-chunk search */
239+
const char *chunks[4];
240+
chunks[0] = buf;
241+
chunks[1] = buf + size;
242+
chunks[2] = NULL;
243+
chunks[3] = NULL;
244+
245+
const char *vstart = NULL, *vend = NULL;
246+
247+
/* Search with empty param name (first param) */
248+
http_find_next_url_param(chunks, "", 0, &vstart, &vend, '&', 0);
249+
250+
/* Search with a 2-byte param name from start */
251+
if (size > 4) {
252+
char pname[3];
253+
memcpy(pname, data, 2);
254+
pname[2] = '\0';
255+
const char *chunks2[4];
256+
chunks2[0] = buf + 2;
257+
chunks2[1] = buf + size;
258+
chunks2[2] = NULL;
259+
chunks2[3] = NULL;
260+
http_find_next_url_param(chunks2, pname, 2, &vstart, &vend, '&', 0);
261+
262+
/* Also try case-insensitive and ';' delimiter */
263+
http_find_next_url_param(chunks2, pname, 2, &vstart, &vend, ';', 1);
264+
}
265+
266+
free(buf);
267+
}
268+
269+
/* Exercise cookie value end finding */
270+
static void fuzz_cookie_value_end(const uint8_t *data, size_t size) {
271+
char *buf = (char *)malloc(size + 1);
272+
if (!buf)
273+
return;
274+
memcpy(buf, data, size);
275+
buf[size] = '\0';
276+
277+
http_find_cookie_value_end(buf, buf + size);
278+
279+
free(buf);
280+
}
281+
282+
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
283+
if (size < 2)
284+
return 0;
285+
286+
/* Use first byte to select which function group to exercise */
287+
uint8_t selector = fuzz_consume_byte(&data, &size);
288+
289+
switch (selector % 7) {
290+
case 0:
291+
fuzz_uri_parsing(data, size);
292+
break;
293+
case 1:
294+
fuzz_content_length(data, size);
295+
break;
296+
case 2:
297+
fuzz_cookie_parsing(data, size);
298+
break;
299+
case 3:
300+
fuzz_header_parsing(data, size);
301+
break;
302+
case 4:
303+
fuzz_misc(data, size);
304+
break;
305+
case 5:
306+
fuzz_url_params(data, size);
307+
break;
308+
case 6:
309+
fuzz_cookie_value_end(data, size);
310+
break;
311+
}
312+
313+
return 0;
314+
}

0 commit comments

Comments
 (0)