Line | Count | Source |
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 | 2.08k | static inline uint8_t fuzz_consume_byte(const uint8_t **data, size_t *size) { |
50 | 2.08k | if (*size == 0) |
51 | 0 | return 0; |
52 | 2.08k | uint8_t b = **data; |
53 | 2.08k | (*data)++; |
54 | 2.08k | (*size)--; |
55 | 2.08k | return b; |
56 | 2.08k | } |
57 | | |
58 | | /* Exercise URI parsing: scheme, authority, path */ |
59 | 113 | static void fuzz_uri_parsing(const uint8_t *data, size_t size) { |
60 | 113 | char *buf = (char *)malloc(size + 1); |
61 | 113 | if (!buf) |
62 | 0 | return; |
63 | 113 | memcpy(buf, data, size); |
64 | 113 | buf[size] = '\0'; |
65 | | |
66 | 113 | struct ist uri = ist2(buf, size); |
67 | 113 | struct http_uri_parser parser = http_uri_parser_init(uri); |
68 | | |
69 | 113 | http_parse_scheme(&parser); |
70 | 113 | http_parse_authority(&parser, 1); |
71 | 113 | http_parse_path(&parser); |
72 | | |
73 | | /* Also test with no_userinfo=0 */ |
74 | 113 | parser = http_uri_parser_init(uri); |
75 | 113 | http_parse_scheme(&parser); |
76 | 113 | http_parse_authority(&parser, 0); |
77 | | |
78 | 113 | free(buf); |
79 | 113 | } |
80 | | |
81 | | /* Exercise Content-Length header parsing — critical for request smuggling */ |
82 | 483 | static void fuzz_content_length(const uint8_t *data, size_t size) { |
83 | 483 | char *buf = (char *)malloc(size + 1); |
84 | 483 | if (!buf) |
85 | 0 | return; |
86 | 483 | memcpy(buf, data, size); |
87 | 483 | buf[size] = '\0'; |
88 | | |
89 | 483 | struct ist value = ist2(buf, size); |
90 | 483 | unsigned long long body_len = 0; |
91 | | |
92 | | /* First occurrence */ |
93 | 483 | int ret = http_parse_cont_len_header(&value, &body_len, 0); |
94 | | |
95 | | /* Simulate a second header with same data (duplicate CL detection) */ |
96 | 483 | if (ret > 0) { |
97 | 244 | struct ist value2 = ist2(buf, size); |
98 | 244 | http_parse_cont_len_header(&value2, &body_len, 1); |
99 | 244 | } |
100 | | |
101 | 483 | free(buf); |
102 | 483 | } |
103 | | |
104 | | /* Exercise cookie extraction */ |
105 | 324 | static void fuzz_cookie_parsing(const uint8_t *data, size_t size) { |
106 | 324 | if (size < 2) |
107 | 1 | return; |
108 | | |
109 | 323 | char *buf = (char *)malloc(size + 1); |
110 | 323 | if (!buf) |
111 | 0 | return; |
112 | 323 | memcpy(buf, data, size); |
113 | 323 | buf[size] = '\0'; |
114 | | |
115 | 323 | char *hdr = buf; |
116 | 323 | const char *hdr_end = buf + size; |
117 | 323 | char *value = NULL; |
118 | 323 | size_t value_l = 0; |
119 | | |
120 | | /* Extract cookie with empty name (match any) */ |
121 | 323 | 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 | 323 | if (size > 6) { |
125 | 216 | char name[5]; |
126 | 216 | memcpy(name, data, 4); |
127 | 216 | name[4] = '\0'; |
128 | 216 | char *search_start = buf + 4; |
129 | 216 | http_extract_cookie_value(search_start, hdr_end, name, 4, 1, |
130 | 216 | &value, &value_l); |
131 | 216 | } |
132 | | |
133 | | /* Test http_extract_next_cookie_name */ |
134 | 323 | char *ptr = NULL; |
135 | 323 | size_t len = 0; |
136 | 323 | http_extract_next_cookie_name(hdr, buf + size, 1, &ptr, &len); |
137 | 323 | http_extract_next_cookie_name(hdr, buf + size, 0, &ptr, &len); |
138 | | |
139 | 323 | free(buf); |
140 | 323 | } |
141 | | |
142 | | /* Exercise header/line parsing utilities */ |
143 | 323 | static void fuzz_header_parsing(const uint8_t *data, size_t size) { |
144 | 323 | char *buf = (char *)malloc(size + 1); |
145 | 323 | if (!buf) |
146 | 0 | return; |
147 | 323 | memcpy(buf, data, size); |
148 | 323 | buf[size] = '\0'; |
149 | | |
150 | 323 | struct ist hdr = ist2(buf, size); |
151 | 323 | struct ist name, value; |
152 | | |
153 | | /* Parse a header line */ |
154 | 323 | http_parse_header(hdr, &name, &value); |
155 | | |
156 | | /* Parse a start line */ |
157 | 323 | struct ist p1, p2, p3; |
158 | 323 | http_parse_stline(hdr, &p1, &p2, &p3); |
159 | | |
160 | | /* Parse status value */ |
161 | 323 | struct ist status, reason; |
162 | 323 | http_parse_status_val(hdr, &status, &reason); |
163 | | |
164 | | /* header_match2 with a known header name */ |
165 | 323 | http_header_match2(buf, buf + size, "content-type", 12); |
166 | 323 | http_header_match2(buf, buf + size, "host", 4); |
167 | | |
168 | | /* find_hdr_value_end */ |
169 | 323 | http_find_hdr_value_end(buf, buf + size); |
170 | | |
171 | 323 | free(buf); |
172 | 323 | } |
173 | | |
174 | | /* Exercise host/port, scheme validation, method lookup, qvalue, etags */ |
175 | 587 | static void fuzz_misc(const uint8_t *data, size_t size) { |
176 | 587 | char *buf = (char *)malloc(size + 1); |
177 | 587 | if (!buf) |
178 | 0 | return; |
179 | 587 | memcpy(buf, data, size); |
180 | 587 | buf[size] = '\0'; |
181 | | |
182 | 587 | struct ist s = ist2(buf, size); |
183 | | |
184 | | /* Host port extraction */ |
185 | 587 | http_get_host_port(s); |
186 | | |
187 | | /* Default port check */ |
188 | 587 | http_is_default_port(IST_NULL, s); |
189 | 587 | http_is_default_port(ist("http://"), s); |
190 | 587 | http_is_default_port(ist("https://"), s); |
191 | | |
192 | | /* Scheme validation */ |
193 | 587 | http_validate_scheme(s); |
194 | | |
195 | | /* Method lookup */ |
196 | 587 | find_http_meth(buf, size); |
197 | | |
198 | | /* Status index */ |
199 | 587 | if (size >= 2) { |
200 | 549 | unsigned int status = (data[0] << 8) | data[1]; |
201 | 549 | http_get_status_idx(status); |
202 | 549 | http_get_reason(status); |
203 | 549 | } |
204 | | |
205 | | /* qvalue parsing */ |
206 | 587 | const char *end = NULL; |
207 | 587 | http_parse_qvalue(buf, &end); |
208 | | |
209 | | /* ETag comparison: split input in half */ |
210 | 587 | if (size >= 4) { |
211 | 294 | size_t half = size / 2; |
212 | 294 | struct ist etag1 = ist2(buf, half); |
213 | 294 | struct ist etag2 = ist2(buf + half, size - half); |
214 | 294 | http_compare_etags(etag1, etag2); |
215 | 294 | } |
216 | | |
217 | | /* Trim leading spaces (safe with standalone buffers) */ |
218 | 587 | 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 | 587 | free(buf); |
225 | 587 | } |
226 | | |
227 | | /* Exercise URL parameter finding */ |
228 | 171 | static void fuzz_url_params(const uint8_t *data, size_t size) { |
229 | 171 | if (size < 4) |
230 | 2 | return; |
231 | | |
232 | 169 | char *buf = (char *)malloc(size + 1); |
233 | 169 | if (!buf) |
234 | 0 | return; |
235 | 169 | memcpy(buf, data, size); |
236 | 169 | buf[size] = '\0'; |
237 | | |
238 | | /* Simple single-chunk search */ |
239 | 169 | const char *chunks[4]; |
240 | 169 | chunks[0] = buf; |
241 | 169 | chunks[1] = buf + size; |
242 | 169 | chunks[2] = NULL; |
243 | 169 | chunks[3] = NULL; |
244 | | |
245 | 169 | const char *vstart = NULL, *vend = NULL; |
246 | | |
247 | | /* Search with empty param name (first param) */ |
248 | 169 | http_find_next_url_param(chunks, "", 0, &vstart, &vend, '&', 0); |
249 | | |
250 | | /* Search with a 2-byte param name from start */ |
251 | 169 | if (size > 4) { |
252 | 153 | char pname[3]; |
253 | 153 | memcpy(pname, data, 2); |
254 | 153 | pname[2] = '\0'; |
255 | 153 | const char *chunks2[4]; |
256 | 153 | chunks2[0] = buf + 2; |
257 | 153 | chunks2[1] = buf + size; |
258 | 153 | chunks2[2] = NULL; |
259 | 153 | chunks2[3] = NULL; |
260 | 153 | http_find_next_url_param(chunks2, pname, 2, &vstart, &vend, '&', 0); |
261 | | |
262 | | /* Also try case-insensitive and ';' delimiter */ |
263 | 153 | http_find_next_url_param(chunks2, pname, 2, &vstart, &vend, ';', 1); |
264 | 153 | } |
265 | | |
266 | 169 | free(buf); |
267 | 169 | } |
268 | | |
269 | | /* Exercise cookie value end finding */ |
270 | 87 | static void fuzz_cookie_value_end(const uint8_t *data, size_t size) { |
271 | 87 | char *buf = (char *)malloc(size + 1); |
272 | 87 | if (!buf) |
273 | 0 | return; |
274 | 87 | memcpy(buf, data, size); |
275 | 87 | buf[size] = '\0'; |
276 | | |
277 | 87 | http_find_cookie_value_end(buf, buf + size); |
278 | | |
279 | 87 | free(buf); |
280 | 87 | } |
281 | | |
282 | 2.08k | int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { |
283 | 2.08k | if (size < 2) |
284 | 1 | return 0; |
285 | | |
286 | | /* Use first byte to select which function group to exercise */ |
287 | 2.08k | uint8_t selector = fuzz_consume_byte(&data, &size); |
288 | | |
289 | 2.08k | switch (selector % 7) { |
290 | 113 | case 0: |
291 | 113 | fuzz_uri_parsing(data, size); |
292 | 113 | break; |
293 | 483 | case 1: |
294 | 483 | fuzz_content_length(data, size); |
295 | 483 | break; |
296 | 324 | case 2: |
297 | 324 | fuzz_cookie_parsing(data, size); |
298 | 324 | break; |
299 | 323 | case 3: |
300 | 323 | fuzz_header_parsing(data, size); |
301 | 323 | break; |
302 | 587 | case 4: |
303 | 587 | fuzz_misc(data, size); |
304 | 587 | break; |
305 | 171 | case 5: |
306 | 171 | fuzz_url_params(data, size); |
307 | 171 | break; |
308 | 87 | case 6: |
309 | 87 | fuzz_cookie_value_end(data, size); |
310 | 87 | break; |
311 | 2.08k | } |
312 | | |
313 | 2.08k | return 0; |
314 | 2.08k | } |