e086c74
commit e086c74
8 files changed
+265 -0
Showing 8 changed files+265-0
▼fuzz/.gitignore
+4-0
| @@ -0,0 +1,4 @@ | ||
1 | +target | |
2 | +corpus | |
3 | +artifacts | |
4 | +coverage | |
0 | 5 | |
▼fuzz/Cargo.toml
+57-0
| @@ -0,0 +1,57 @@ | ||
1 | +[package] | |
2 | +name = "justapi-fuzz" | |
3 | +version = "0.1.0" | |
4 | +edition = "2021" | |
5 | + | |
6 | +[package.metadata] | |
7 | +cargo-fuzz = true | |
8 | + | |
9 | +[dependencies] | |
10 | +libfuzzer-sys = "0.4" | |
11 | +justapi-core = { path = "../crates/justapi-core" } | |
12 | +jsonwebtoken = "9" | |
13 | +serde_json = "1" | |
14 | +http = "1" | |
15 | +serde_urlencoded = "0.7" | |
16 | + | |
17 | +[[bin]] | |
18 | +name = "fuzz_router" | |
19 | +path = "fuzz_targets/fuzz_router.rs" | |
20 | +test = false | |
21 | +doc = false | |
22 | +bench = false | |
23 | + | |
24 | +[[bin]] | |
25 | +name = "fuzz_file_paths" | |
26 | +path = "fuzz_targets/fuzz_file_paths.rs" | |
27 | +test = false | |
28 | +doc = false | |
29 | +bench = false | |
30 | + | |
31 | +[[bin]] | |
32 | +name = "fuzz_body" | |
33 | +path = "fuzz_targets/fuzz_body.rs" | |
34 | +test = false | |
35 | +doc = false | |
36 | +bench = false | |
37 | + | |
38 | +[[bin]] | |
39 | +name = "fuzz_query_params" | |
40 | +path = "fuzz_targets/fuzz_query_params.rs" | |
41 | +test = false | |
42 | +doc = false | |
43 | +bench = false | |
44 | + | |
45 | +[[bin]] | |
46 | +name = "fuzz_headers" | |
47 | +path = "fuzz_targets/fuzz_headers.rs" | |
48 | +test = false | |
49 | +doc = false | |
50 | +bench = false | |
51 | + | |
52 | +[[bin]] | |
53 | +name = "fuzz_jwt" | |
54 | +path = "fuzz_targets/fuzz_jwt.rs" | |
55 | +test = false | |
56 | +doc = false | |
57 | +bench = false | |
0 | 58 | |
▼fuzz/fuzz_targets/fuzz_body.rs
+25-0
| @@ -0,0 +1,25 @@ | ||
1 | +#![no_main] | |
2 | + | |
3 | +use libfuzzer_sys::fuzz_target; | |
4 | + | |
5 | +fuzz_target!(|data: &[u8]| { | |
6 | + if data.len() < 2 { | |
7 | + return; | |
8 | + } | |
9 | + | |
10 | + // Fuzz JSON body parsing (used by request validation) | |
11 | + let _: Result<serde_json::Value, _> = serde_json::from_slice(data); | |
12 | + | |
13 | + // Fuzz JSON Schema validation | |
14 | + let schema = r#"{"type":"object","properties":{"name":{"type":"string"},"age":{"type":"integer","minimum":0}},"required":["name"]}"#; | |
15 | + let _ = justapi_core::validate::validate_json_schema(data, schema); | |
16 | + | |
17 | + // Fuzz with recursive schema | |
18 | + let recursive_schema = r##"{"type":"object","additionalProperties":{"$ref":"#"}}"##; | |
19 | + let _ = justapi_core::validate::validate_json_schema(data, recursive_schema); | |
20 | + | |
21 | + // Fuzz deeply nested JSON parsing (stack depth) | |
22 | + if data.len() > 10 { | |
23 | + let _: Result<serde_json::Value, _> = serde_json::from_slice(data); | |
24 | + } | |
25 | +}); | |
0 | 26 | |
▼fuzz/fuzz_targets/fuzz_file_paths.rs
+62-0
| @@ -0,0 +1,62 @@ | ||
1 | +#![no_main] | |
2 | + | |
3 | +use libfuzzer_sys::fuzz_target; | |
4 | +use std::path::Path; | |
5 | + | |
6 | +fuzz_target!(|data: &[u8]| { | |
7 | + if data.is_empty() { | |
8 | + return; | |
9 | + } | |
10 | + | |
11 | + if let Ok(s) = std::str::from_utf8(data) { | |
12 | + // Sanitize path: prevent directory traversal | |
13 | + let path = Path::new(s); | |
14 | + let _ = path.components().collect::<Vec<_>>(); | |
15 | + | |
16 | + // Check for path traversal patterns | |
17 | + let normalized = s.replace('\\', "/"); | |
18 | + let has_traversal = normalized.contains(".."); | |
19 | + let has_null = s.contains('\0'); | |
20 | + let has_absolute = normalized.starts_with('/'); | |
21 | + | |
22 | + // Simulate path join and canonicalization | |
23 | + let base = Path::new("/var/www/static"); | |
24 | + let joined = base.join(path); | |
25 | + let _ = joined.strip_prefix("/var/www/static"); | |
26 | + | |
27 | + // Fuzz MIME type detection by extension | |
28 | + if let Some(ext) = path.extension().and_then(|e| e.to_str()) { | |
29 | + let mime = match ext.to_lowercase().as_str() { | |
30 | + "html" | "htm" => "text/html", | |
31 | + "css" => "text/css", | |
32 | + "js" => "application/javascript", | |
33 | + "json" => "application/json", | |
34 | + "png" => "image/png", | |
35 | + "jpg" | "jpeg" => "image/jpeg", | |
36 | + "gif" => "image/gif", | |
37 | + "svg" => "image/svg+xml", | |
38 | + "ico" => "image/x-icon", | |
39 | + "woff2" => "font/woff2", | |
40 | + _ => "application/octet-stream", | |
41 | + }; | |
42 | + let _ = mime; | |
43 | + } | |
44 | + | |
45 | + // Fuzz percent-decoding (as used in URL path segments) | |
46 | + let mut decoded = String::with_capacity(s.len()); | |
47 | + let mut chars = s.chars(); | |
48 | + while let Some(c) = chars.next() { | |
49 | + if c == '%' { | |
50 | + let hex: String = chars.by_ref().take(2).collect(); | |
51 | + if hex.len() == 2 { | |
52 | + if let Ok(byte) = u8::from_str_radix(&hex, 16) { | |
53 | + decoded.push(byte as char); | |
54 | + } | |
55 | + } | |
56 | + } else { | |
57 | + decoded.push(c); | |
58 | + } | |
59 | + } | |
60 | + let _ = decoded; | |
61 | + } | |
62 | +}); | |
0 | 63 | |
▼fuzz/fuzz_targets/fuzz_headers.rs
+48-0
| @@ -0,0 +1,48 @@ | ||
1 | +#![no_main] | |
2 | + | |
3 | +use libfuzzer_sys::fuzz_target; | |
4 | +use http::{HeaderName, HeaderValue, Request, Uri, Method, Version}; | |
5 | + | |
6 | +fuzz_target!(|data: &[u8]| { | |
7 | + if data.is_empty() { | |
8 | + return; | |
9 | + } | |
10 | + | |
11 | + // Try to parse as raw header name+value pairs | |
12 | + if let Ok(s) = std::str::from_utf8(data) { | |
13 | + for line in s.lines().take(10) { | |
14 | + if let Some(pos) = line.find(':') { | |
15 | + let name_str = &line[..pos].trim(); | |
16 | + let val_str = &line[pos + 1..].trim(); | |
17 | + let _ = HeaderName::from_bytes(name_str.as_bytes()); | |
18 | + let _ = HeaderValue::from_bytes(val_str.as_bytes()); | |
19 | + } | |
20 | + } | |
21 | + } | |
22 | + | |
23 | + // Fuzz HeaderName directly | |
24 | + let _ = HeaderName::from_bytes(data); | |
25 | + | |
26 | + // Fuzz HeaderValue directly | |
27 | + let _ = HeaderValue::from_bytes(data); | |
28 | + | |
29 | + // Fuzz URI parsing | |
30 | + if let Ok(s) = std::str::from_utf8(data) { | |
31 | + let _: Result<Uri, _> = s.parse(); | |
32 | + } | |
33 | + | |
34 | + // Fuzz Method parsing | |
35 | + if let Ok(s) = std::str::from_utf8(data) { | |
36 | + let _: Result<Method, _> = s.parse(); | |
37 | + } | |
38 | + | |
39 | + // Fuzz Version parsing | |
40 | + let _ = match data.first().copied().unwrap_or(0) { | |
41 | + 0 => Version::HTTP_09, | |
42 | + 1 => Version::HTTP_10, | |
43 | + 2 => Version::HTTP_11, | |
44 | + 3 => Version::HTTP_2, | |
45 | + 4 => Version::HTTP_3, | |
46 | + _ => Version::HTTP_11, | |
47 | + }; | |
48 | +}); | |
0 | 49 | |
▼fuzz/fuzz_targets/fuzz_jwt.rs
+22-0
| @@ -0,0 +1,22 @@ | ||
1 | +#![no_main] | |
2 | + | |
3 | +use libfuzzer_sys::fuzz_target; | |
4 | +use jsonwebtoken::{decode, decode_header, Validation, DecodingKey, Algorithm}; | |
5 | + | |
6 | +fuzz_target!(|data: &[u8]| { | |
7 | + if data.len() < 4 { | |
8 | + return; | |
9 | + } | |
10 | + let key = DecodingKey::from_secret(b"test-secret-key-for-fuzzing"); | |
11 | + let validation = Validation::new(Algorithm::HS256); | |
12 | + | |
13 | + if let Ok(s) = std::str::from_utf8(data) { | |
14 | + let _ = decode_header(s); | |
15 | + let _ = decode::<serde_json::Value>(s, &key, &validation); | |
16 | + } | |
17 | + | |
18 | + let alt_key = DecodingKey::from_secret(data); | |
19 | + if let Ok(s) = std::str::from_utf8(data) { | |
20 | + let _ = decode::<serde_json::Value>(s, &alt_key, &validation); | |
21 | + } | |
22 | +}); | |
0 | 23 | |
▼fuzz/fuzz_targets/fuzz_query_params.rs
+21-0
| @@ -0,0 +1,21 @@ | ||
1 | +#![no_main] | |
2 | + | |
3 | +use libfuzzer_sys::fuzz_target; | |
4 | +use std::collections::HashMap; | |
5 | + | |
6 | +fuzz_target!(|data: &[u8]| { | |
7 | + if data.is_empty() { | |
8 | + return; | |
9 | + } | |
10 | + | |
11 | + if let Ok(s) = std::str::from_utf8(data) { | |
12 | + // Fuzz URL query string parsing | |
13 | + let _: HashMap<String, String> = serde_urlencoded::from_str(s).unwrap_or_default(); | |
14 | + | |
15 | + // Fuzz full URL parsing with query parameters | |
16 | + if s.len() > 4 && s.contains('=') { | |
17 | + let url = format!("http://localhost:8080/test?{}", s); | |
18 | + let _: Result<http::Uri, _> = url.parse(); | |
19 | + } | |
20 | + } | |
21 | +}); | |
0 | 22 | |
▼fuzz/fuzz_targets/fuzz_router.rs
+26-0
| @@ -0,0 +1,26 @@ | ||
1 | +#![no_main] | |
2 | + | |
3 | +use libfuzzer_sys::fuzz_target; | |
4 | + | |
5 | +fuzz_target!(|data: &[u8]| { | |
6 | + if data.is_empty() { | |
7 | + return; | |
8 | + } | |
9 | + | |
10 | + if let Ok(s) = std::str::from_utf8(data) { | |
11 | + // Build a router with sample routes and fuzz the path matching | |
12 | + use http::Method; | |
13 | + let mut router = justapi_core::router::Router::new(); | |
14 | + let _ = router.insert(Method::GET, "/users/{id}", 1usize); | |
15 | + let _ = router.insert(Method::GET, "/users/{id}/posts", 2); | |
16 | + let _ = router.insert(Method::GET, "/users/{id}/posts/{post_id}", 3); | |
17 | + let _ = router.insert(Method::GET, "/static/{*path}", 4); | |
18 | + let _ = router.insert(Method::GET, "/api/v1/{resource}", 5); | |
19 | + let _ = router.insert(Method::GET, "/api/v1/{resource}/{action}", 6); | |
20 | + let _ = router.insert(Method::GET, "/health", 7); | |
21 | + let _ = router.insert(Method::GET, "/live", 8); | |
22 | + let _ = router.insert(Method::GET, "/ready", 9); | |
23 | + | |
24 | + let _ = router.at(&Method::GET, s); | |
25 | + } | |
26 | +}); | |
0 | 27 | |