/src/http_message_fuzzer.cc
Line | Count | Source |
1 | | /* Copyright 2026 Google LLC |
2 | | Licensed under the Apache License, Version 2.0 (the "License"); |
3 | | you may not use this file except in compliance with the License. |
4 | | You may obtain a copy of the License at |
5 | | http://www.apache.org/licenses/LICENSE-2.0 |
6 | | Unless required by applicable law or agreed to in writing, software |
7 | | distributed under the License is distributed on an "AS IS" BASIS, |
8 | | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
9 | | See the License for the specific language governing permissions and |
10 | | limitations under the License. |
11 | | */ |
12 | | |
13 | | /* Fuzzer for the full HTTP parsing state machine: |
14 | | * - Request line and response line parsing with all HTTP methods |
15 | | * - Header parsing including continuation lines |
16 | | * - Body reading with Content-Length |
17 | | * - Chunked transfer encoding |
18 | | * - Trailer headers after chunked body |
19 | | * - URI parsing with various flags |
20 | | */ |
21 | | |
22 | | #include <stddef.h> |
23 | | #include <stdint.h> |
24 | | #include <stdlib.h> |
25 | | #include <string.h> |
26 | | |
27 | | extern "C" { |
28 | | #include "libevent/include/event2/event.h" |
29 | | #include "libevent/include/event2/buffer.h" |
30 | | #include "libevent/include/event2/http.h" |
31 | | #include "libevent/include/event2/http_struct.h" |
32 | | #include "libevent/include/event2/keyvalq_struct.h" |
33 | | #include "libevent/http-internal.h" |
34 | | } |
35 | | |
36 | 0 | extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { |
37 | 0 | if (size < 6) |
38 | 0 | return 0; |
39 | | |
40 | | /* Use first 2 bytes as control */ |
41 | 0 | uint8_t mode = data[0]; |
42 | 0 | uint8_t extra = data[1]; |
43 | 0 | data += 2; |
44 | 0 | size -= 2; |
45 | |
|
46 | 0 | struct evhttp *http_val = evhttp_new(NULL); |
47 | 0 | if (!http_val) |
48 | 0 | return 0; |
49 | | |
50 | | /* Set up a fake connection context for request parsing */ |
51 | 0 | struct evhttp_connection evcon; |
52 | 0 | memset(&evcon, 0, sizeof(evcon)); |
53 | 0 | evcon.ext_method_cmp = NULL; |
54 | 0 | evcon.max_headers_size = (extra % 4 == 0) ? 256 : 8192; |
55 | 0 | evcon.http_server = http_val; |
56 | | |
57 | | /* === Parse as HTTP request with proper firstline -> headers flow === */ |
58 | 0 | if (mode & 0x01) { |
59 | 0 | struct evhttp_request *req = evhttp_request_new(NULL, NULL); |
60 | 0 | if (req) { |
61 | 0 | req->kind = EVHTTP_REQUEST; |
62 | 0 | req->evcon = &evcon; |
63 | |
|
64 | 0 | struct evbuffer *buf = evbuffer_new(); |
65 | 0 | if (buf) { |
66 | 0 | evbuffer_add(buf, data, size); |
67 | |
|
68 | 0 | enum message_read_status s = evhttp_parse_firstline_(req, buf); |
69 | 0 | if (s == ALL_DATA_READ) { |
70 | | /* Successfully parsed request line, now parse headers */ |
71 | 0 | s = evhttp_parse_headers_(req, buf); |
72 | 0 | if (s == ALL_DATA_READ) { |
73 | | /* Successfully parsed headers, examine parsed data */ |
74 | 0 | evhttp_request_get_host(req); |
75 | 0 | evhttp_request_get_uri(req); |
76 | 0 | evhttp_request_get_command(req); |
77 | |
|
78 | 0 | struct evkeyvalq *hdrs = evhttp_request_get_input_headers(req); |
79 | 0 | if (hdrs) { |
80 | 0 | evhttp_find_header(hdrs, "Content-Length"); |
81 | 0 | evhttp_find_header(hdrs, "Transfer-Encoding"); |
82 | 0 | evhttp_find_header(hdrs, "Connection"); |
83 | 0 | evhttp_find_header(hdrs, "Host"); |
84 | 0 | evhttp_find_header(hdrs, "Expect"); |
85 | 0 | evhttp_find_header(hdrs, "Content-Type"); |
86 | 0 | } |
87 | |
|
88 | 0 | const struct evhttp_uri *uri = evhttp_request_get_evhttp_uri(req); |
89 | 0 | if (uri) { |
90 | 0 | evhttp_uri_get_scheme(uri); |
91 | 0 | evhttp_uri_get_host(uri); |
92 | 0 | evhttp_uri_get_port(uri); |
93 | 0 | evhttp_uri_get_path(uri); |
94 | 0 | evhttp_uri_get_query(uri); |
95 | 0 | evhttp_uri_get_fragment(uri); |
96 | 0 | evhttp_uri_get_userinfo(uri); |
97 | 0 | } |
98 | | |
99 | | /* Move remaining data as body */ |
100 | 0 | size_t rem = evbuffer_get_length(buf); |
101 | 0 | if (rem > 0) { |
102 | 0 | struct evbuffer *body = evhttp_request_get_input_buffer(req); |
103 | 0 | if (body) |
104 | 0 | evbuffer_add_buffer(body, buf); |
105 | 0 | } |
106 | 0 | } |
107 | 0 | } |
108 | 0 | evbuffer_free(buf); |
109 | 0 | } |
110 | 0 | evhttp_request_free(req); |
111 | 0 | } |
112 | 0 | } |
113 | | |
114 | | /* === Parse as HTTP response === */ |
115 | 0 | if (mode & 0x02) { |
116 | 0 | struct evhttp_request *req = evhttp_request_new(NULL, NULL); |
117 | 0 | if (req) { |
118 | 0 | req->kind = EVHTTP_RESPONSE; |
119 | 0 | req->evcon = &evcon; |
120 | |
|
121 | 0 | struct evbuffer *buf = evbuffer_new(); |
122 | 0 | if (buf) { |
123 | 0 | evbuffer_add(buf, data, size); |
124 | |
|
125 | 0 | enum message_read_status s = evhttp_parse_firstline_(req, buf); |
126 | 0 | if (s == ALL_DATA_READ) { |
127 | 0 | s = evhttp_parse_headers_(req, buf); |
128 | 0 | if (s == ALL_DATA_READ) { |
129 | 0 | int code = evhttp_request_get_response_code(req); |
130 | 0 | (void)code; |
131 | 0 | const char *reason = evhttp_request_get_response_code_line(req); |
132 | 0 | (void)reason; |
133 | |
|
134 | 0 | struct evkeyvalq *hdrs = evhttp_request_get_input_headers(req); |
135 | 0 | if (hdrs) { |
136 | 0 | evhttp_find_header(hdrs, "Content-Length"); |
137 | 0 | evhttp_find_header(hdrs, "Transfer-Encoding"); |
138 | 0 | evhttp_find_header(hdrs, "Set-Cookie"); |
139 | 0 | evhttp_find_header(hdrs, "Location"); |
140 | 0 | } |
141 | 0 | } |
142 | 0 | } |
143 | 0 | evbuffer_free(buf); |
144 | 0 | } |
145 | 0 | evhttp_request_free(req); |
146 | 0 | } |
147 | 0 | } |
148 | | |
149 | | /* === Parse request with prepended valid method lines to stress header parsing === */ |
150 | 0 | if (mode & 0x04) { |
151 | 0 | static const char *methods[] = { |
152 | 0 | "POST / HTTP/1.1\r\n", |
153 | 0 | "PUT /resource HTTP/1.1\r\n", |
154 | 0 | "DELETE /item HTTP/1.1\r\n", |
155 | 0 | "PATCH /update HTTP/1.1\r\n", |
156 | 0 | "OPTIONS * HTTP/1.1\r\n", |
157 | 0 | "PROPFIND /dav HTTP/1.1\r\n", |
158 | 0 | "PROPPATCH /dav HTTP/1.1\r\n", |
159 | 0 | "MKCOL /dir HTTP/1.1\r\n", |
160 | 0 | "LOCK /file HTTP/1.1\r\n", |
161 | 0 | "UNLOCK /file HTTP/1.1\r\n", |
162 | 0 | "COPY /src HTTP/1.1\r\n", |
163 | 0 | "MOVE /src HTTP/1.1\r\n", |
164 | 0 | }; |
165 | 0 | const char *method = methods[extra % 12]; |
166 | |
|
167 | 0 | struct evhttp_request *req = evhttp_request_new(NULL, NULL); |
168 | 0 | if (req) { |
169 | 0 | req->kind = EVHTTP_REQUEST; |
170 | 0 | req->evcon = &evcon; |
171 | |
|
172 | 0 | struct evbuffer *buf = evbuffer_new(); |
173 | 0 | if (buf) { |
174 | 0 | evbuffer_add(buf, method, strlen(method)); |
175 | 0 | evbuffer_add(buf, data, size); |
176 | |
|
177 | 0 | enum message_read_status s = evhttp_parse_firstline_(req, buf); |
178 | 0 | if (s == ALL_DATA_READ) { |
179 | 0 | s = evhttp_parse_headers_(req, buf); |
180 | 0 | if (s == ALL_DATA_READ) { |
181 | 0 | evhttp_request_get_host(req); |
182 | 0 | evhttp_request_get_command(req); |
183 | 0 | } |
184 | 0 | } |
185 | 0 | evbuffer_free(buf); |
186 | 0 | } |
187 | 0 | evhttp_request_free(req); |
188 | 0 | } |
189 | 0 | } |
190 | | |
191 | | /* === Parse response with prepended valid status line to stress header parsing === */ |
192 | 0 | if (mode & 0x08) { |
193 | 0 | static const char *status_lines[] = { |
194 | 0 | "HTTP/1.1 200 OK\r\n", |
195 | 0 | "HTTP/1.0 404 Not Found\r\n", |
196 | 0 | "HTTP/1.1 301 Moved Permanently\r\n", |
197 | 0 | "HTTP/1.1 500 Internal Server Error\r\n", |
198 | 0 | "HTTP/1.1 100 Continue\r\n", |
199 | 0 | "HTTP/1.1 204 No Content\r\n", |
200 | 0 | "HTTP/1.0 302 Found\r\n", |
201 | 0 | "HTTP/1.1 403 Forbidden\r\n", |
202 | 0 | }; |
203 | 0 | const char *status = status_lines[extra % 8]; |
204 | |
|
205 | 0 | struct evhttp_request *req = evhttp_request_new(NULL, NULL); |
206 | 0 | if (req) { |
207 | 0 | req->kind = EVHTTP_RESPONSE; |
208 | 0 | req->evcon = &evcon; |
209 | |
|
210 | 0 | struct evbuffer *buf = evbuffer_new(); |
211 | 0 | if (buf) { |
212 | 0 | evbuffer_add(buf, status, strlen(status)); |
213 | 0 | evbuffer_add(buf, data, size); |
214 | |
|
215 | 0 | enum message_read_status s = evhttp_parse_firstline_(req, buf); |
216 | 0 | if (s == ALL_DATA_READ) { |
217 | 0 | s = evhttp_parse_headers_(req, buf); |
218 | 0 | if (s == ALL_DATA_READ) { |
219 | 0 | evhttp_request_get_response_code(req); |
220 | 0 | evhttp_request_get_response_code_line(req); |
221 | |
|
222 | 0 | struct evkeyvalq *hdrs = evhttp_request_get_input_headers(req); |
223 | 0 | if (hdrs) { |
224 | 0 | evhttp_find_header(hdrs, "Content-Type"); |
225 | 0 | evhttp_find_header(hdrs, "Server"); |
226 | 0 | } |
227 | 0 | } |
228 | 0 | } |
229 | 0 | evbuffer_free(buf); |
230 | 0 | } |
231 | 0 | evhttp_request_free(req); |
232 | 0 | } |
233 | 0 | } |
234 | | |
235 | | /* === Exercise evhttp_decode_uri with percent-encoded data === */ |
236 | 0 | if (mode & 0x10) { |
237 | 0 | char *uri_str = (char *)malloc(size + 1); |
238 | 0 | if (uri_str) { |
239 | 0 | memcpy(uri_str, data, size); |
240 | 0 | uri_str[size] = '\0'; |
241 | |
|
242 | 0 | char *decoded = evhttp_decode_uri(uri_str); |
243 | 0 | if (decoded) |
244 | 0 | free(decoded); |
245 | |
|
246 | 0 | char *encoded = evhttp_encode_uri(uri_str); |
247 | 0 | if (encoded) |
248 | 0 | free(encoded); |
249 | |
|
250 | 0 | free(uri_str); |
251 | 0 | } |
252 | 0 | } |
253 | | |
254 | | /* === Exercise evhttp_parse_query_str_flags === */ |
255 | 0 | if (mode & 0x20) { |
256 | 0 | char *query = (char *)malloc(size + 1); |
257 | 0 | if (query) { |
258 | 0 | memcpy(query, data, size); |
259 | 0 | query[size] = '\0'; |
260 | |
|
261 | 0 | struct evkeyvalq params; |
262 | 0 | TAILQ_INIT(¶ms); |
263 | 0 | int flags = (extra & 0x0F); |
264 | 0 | evhttp_parse_query_str_flags(query, ¶ms, flags); |
265 | | |
266 | | /* Iterate and free all parsed parameters */ |
267 | 0 | struct evkeyval *kv; |
268 | 0 | while ((kv = TAILQ_FIRST(¶ms)) != NULL) { |
269 | 0 | TAILQ_REMOVE(¶ms, kv, next); |
270 | 0 | free(kv->key); |
271 | 0 | free(kv->value); |
272 | 0 | free(kv); |
273 | 0 | } |
274 | |
|
275 | 0 | free(query); |
276 | 0 | } |
277 | 0 | } |
278 | | |
279 | | /* === Exercise header add/remove/clear operations === */ |
280 | 0 | if (mode & 0x40) { |
281 | 0 | struct evkeyvalq headers; |
282 | 0 | TAILQ_INIT(&headers); |
283 | | |
284 | | /* Split fuzz data into key/value pairs to add as headers */ |
285 | 0 | size_t pos = 0; |
286 | 0 | int count = 0; |
287 | 0 | while (pos < size && count < 32) { |
288 | | /* Find a separator for key */ |
289 | 0 | size_t key_end = pos; |
290 | 0 | while (key_end < size && data[key_end] != ':' && data[key_end] != '\0') |
291 | 0 | key_end++; |
292 | 0 | if (key_end >= size) break; |
293 | | |
294 | 0 | size_t val_start = key_end + 1; |
295 | 0 | size_t val_end = val_start; |
296 | 0 | while (val_end < size && data[val_end] != '\n' && data[val_end] != '\0') |
297 | 0 | val_end++; |
298 | |
|
299 | 0 | if (key_end > pos) { |
300 | 0 | char *key = (char *)malloc(key_end - pos + 1); |
301 | 0 | char *val = (char *)malloc(val_end - val_start + 1); |
302 | 0 | if (key && val) { |
303 | 0 | memcpy(key, data + pos, key_end - pos); |
304 | 0 | key[key_end - pos] = '\0'; |
305 | 0 | memcpy(val, data + val_start, val_end - val_start); |
306 | 0 | val[val_end - val_start] = '\0'; |
307 | |
|
308 | 0 | evhttp_add_header(&headers, key, val); |
309 | 0 | count++; |
310 | 0 | } |
311 | 0 | free(key); |
312 | 0 | free(val); |
313 | 0 | } |
314 | 0 | pos = val_end + 1; |
315 | 0 | } |
316 | | |
317 | | /* Try to find some headers */ |
318 | 0 | if (count > 0) { |
319 | 0 | evhttp_find_header(&headers, "Content-Type"); |
320 | 0 | evhttp_find_header(&headers, "Host"); |
321 | 0 | } |
322 | |
|
323 | 0 | evhttp_clear_headers(&headers); |
324 | 0 | } |
325 | |
|
326 | 0 | evhttp_free(http_val); |
327 | 0 | return 0; |
328 | 0 | } |