Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/cifuzz.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: CIFuzz
on:
pull_request:
branches:
- main

permissions: {}

jobs:
Fuzzing:
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- name: Build Fuzzers
id: build
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
with:
oss-fuzz-project-name: "rust-url"
language: rust
- name: Run Fuzzers
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
with:
oss-fuzz-project-name: "rust-url"
language: rust
fuzz-seconds: 600
- name: Upload Crash
uses: actions/upload-artifact@v4
if: failure() && steps.build.outcome == 'success'
with:
name: artifacts
path: ./out/artifacts
57 changes: 57 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
[package]
name = "rust-url-fuzz"
version = "0.0.1"
authors = ["Automatically generated"]
publish = false
edition = "2021"

[package.metadata]
cargo-fuzz = true

[dependencies]
libfuzzer-sys = "0.4"
url = { path = "../url" }
idna = { path = "../idna", features = ["std"] }
percent-encoding = { path = "../percent_encoding", features = ["alloc"] }
form_urlencoded = { path = "../form_urlencoded", features = ["alloc"] }
data-url = { path = "../data-url", features = ["std"] }

# --- Fuzz targets ---

[[bin]]
name = "fuzz_url_parse_roundtrip"
path = "fuzz_targets/fuzz_url_parse_roundtrip.rs"
doc = false

[[bin]]
name = "fuzz_url_differential"
path = "fuzz_targets/fuzz_url_differential.rs"
doc = false

[[bin]]
name = "fuzz_url_setters"
path = "fuzz_targets/fuzz_url_setters.rs"
doc = false

[[bin]]
name = "fuzz_idna"
path = "fuzz_targets/fuzz_idna.rs"
doc = false

[[bin]]
name = "fuzz_data_url"
path = "fuzz_targets/fuzz_data_url.rs"
doc = false

[[bin]]
name = "fuzz_form_urlencoded"
path = "fuzz_targets/fuzz_form_urlencoded.rs"
doc = false

[[bin]]
name = "fuzz_percent_encoding"
path = "fuzz_targets/fuzz_percent_encoding.rs"
doc = false

[workspace]
members = ["."]
1 change: 1 addition & 0 deletions fuzz/corpus/seed/idna_01
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
münchen.de
1 change: 1 addition & 0 deletions fuzz/corpus/seed/idna_02
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
xn--mnchen-3ya.de
1 change: 1 addition & 0 deletions fuzz/corpus/seed/url_01
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
https://example.com/path?query=value#fragment
1 change: 1 addition & 0 deletions fuzz/corpus/seed/url_02
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
http://user:password@host.example.com:8080/path/to/resource?key=val&key2=val2#frag
1 change: 1 addition & 0 deletions fuzz/corpus/seed/url_03
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ftp://ftp.example.com/pub/files/readme.txt
1 change: 1 addition & 0 deletions fuzz/corpus/seed/url_04
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
file:///tmp/local/file.txt
1 change: 1 addition & 0 deletions fuzz/corpus/seed/url_05
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
https://[::1]:443/ipv6
1 change: 1 addition & 0 deletions fuzz/corpus/seed/url_06
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
https://xn--nxasmq6b.example.com/idn
1 change: 1 addition & 0 deletions fuzz/corpus/seed/url_07
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
data:text/plain;base64,SGVsbG8gV29ybGQh
1 change: 1 addition & 0 deletions fuzz/corpus/seed/url_08
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
data:text/html,%3Ch1%3EHello%3C%2Fh1%3E
1 change: 1 addition & 0 deletions fuzz/corpus/seed/url_09
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
https://example.com/path%20with%20spaces?q=%E4%B8%AD%E6%96%87
1 change: 1 addition & 0 deletions fuzz/corpus/seed/url_10
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
https://example.com/?foo=bar&baz=qux&empty=&key+with+plus=value+with+plus
81 changes: 81 additions & 0 deletions fuzz/fuzz.dict
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# URL schemes
"http://"
"https://"
"ftp://"
"file://"
"data:"
"blob:"
"ws://"
"wss://"
"custom://"

# URL delimiters
"://"
":/"
"//"
"/"
"?"
"#"
"@"
":"
";"

# Common URL components
"example.com"
"localhost"
"127.0.0.1"
"[::1]"
"[2001:db8::1]"
"0.0.0.0"

# Percent encoding
"%00"
"%20"
"%25"
"%2F"
"%3A"
"%3F"
"%40"
"%23"
"%26"
"%3D"
"%C3%A9"
"%E4%B8%AD"

# Form URL encoded
"&"
"="
"+"
"key=value"
"a=b&c=d"

# IDNA / Punycode
"xn--"
"xn--nxasmq6b"
".com"
".de"
".org"

# Data URL
"data:,"
"data:text/plain,"
"data:text/plain;base64,"
"data:text/html,"
"data:application/octet-stream;base64,"
";base64"
";charset=utf-8"
";charset=US-ASCII"

# Base64
"SGVsbG8="
"AAAA"
"////+"

# Special characters
"\x09"
"\x0a"
"\x0d"
" "
"\x5c"
".."
"."
48 changes: 48 additions & 0 deletions fuzz/fuzz_targets/fuzz_data_url.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#![no_main]

use libfuzzer_sys::fuzz_target;
use std::str;

fuzz_target!(|data: &[u8]| {
let Ok(utf8) = str::from_utf8(data) else {
return;
};

let Ok(data_url) = data_url::DataUrl::process(utf8) else {
return;
};

// Access MIME type (should not panic)
let mime = data_url.mime_type();
let _ = mime.type_.len();
let _ = mime.subtype.len();
for (name, value) in &mime.parameters {
let _ = name.len();
let _ = value.len();
}

// Decode body (should not panic)
match data_url.decode_to_vec() {
Ok((body, fragment)) => {
// Body must be valid bytes
let _ = body.len();
if let Some(frag) = fragment {
// Fragment percent-encoding should produce valid UTF-8
let _ = frag.to_percent_encoded();
}
}
Err(_) => {
// Base64 decode errors are expected for malformed input
}
}

// Test streaming decode
let mut chunks = Vec::new();
let _ = data_url.decode(|bytes| {
chunks.push(bytes.to_vec());
Ok::<(), std::convert::Infallible>(())
});

// Test forgiving_base64 directly
let _ = data_url::forgiving_base64::decode_to_vec(data);
});
35 changes: 35 additions & 0 deletions fuzz/fuzz_targets/fuzz_form_urlencoded.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#![no_main]

use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &[u8]| {
// Parse the input as form-urlencoded data
let pairs: Vec<(String, String)> = form_urlencoded::parse(data)
.into_owned()
.collect();

// Roundtrip invariant: serialize and re-parse should produce the same pairs
let mut serializer = form_urlencoded::Serializer::new(String::new());
for (name, value) in &pairs {
serializer.append_pair(name, value);
}
let serialized = serializer.finish();

let reparsed: Vec<(String, String)> = form_urlencoded::parse(serialized.as_bytes())
.into_owned()
.collect();

// The key insight: form_urlencoded uses lossy UTF-8 decoding,
// so we need to compare the parsed pairs (not raw bytes).
// After one roundtrip through parse->serialize->parse, the result should be stable.
assert_eq!(
pairs, reparsed,
"form_urlencoded roundtrip mismatch: serialized={:?}",
serialized
);

// Test byte_serialize roundtrip
let byte_serialized: String = form_urlencoded::byte_serialize(data).collect();
// byte_serialize output should be valid UTF-8 (it produces &str slices)
let _ = byte_serialized.len();
});
64 changes: 64 additions & 0 deletions fuzz/fuzz_targets/fuzz_idna.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#![no_main]

use libfuzzer_sys::fuzz_target;
use std::str;

fuzz_target!(|data: &[u8]| {
// Test domain_to_ascii_cow (primary entry point, takes &[u8])
let _ = idna::domain_to_ascii_cow(data, idna::AsciiDenyList::URL);
let _ = idna::domain_to_ascii_cow(data, idna::AsciiDenyList::EMPTY);
let _ = idna::domain_to_ascii_cow(data, idna::AsciiDenyList::STD3);

let Ok(utf8) = str::from_utf8(data) else {
return;
};

// Test domain_to_ascii (takes &str)
let ascii_result = idna::domain_to_ascii(utf8);
let strict_result = idna::domain_to_ascii_strict(utf8);

// Roundtrip invariant: if we can convert to ASCII, converting to Unicode
// and back to ASCII should produce the same result
if let Ok(ref ascii) = ascii_result {
let (unicode, unicode_result) = idna::domain_to_unicode(ascii);
if unicode_result.is_ok() {
if let Ok(back_to_ascii) = idna::domain_to_ascii(&unicode) {
assert_eq!(
ascii.to_lowercase(),
back_to_ascii.to_lowercase(),
"IDNA roundtrip mismatch: input={:?}, ascii={:?}, unicode={:?}, back={:?}",
utf8,
ascii,
unicode,
back_to_ascii
);
}
}
}

// Consistency: strict mode should be a subset of non-strict
if strict_result.is_ok() {
assert!(
ascii_result.is_ok(),
"strict succeeded but non-strict failed for {:?}",
utf8
);
}

// Test domain_to_unicode
let (unicode_str, _result) = idna::domain_to_unicode(utf8);

// The Unicode result should itself be valid UTF-8 (it's a String)
let _ = unicode_str.len();

// Test Punycode encode/decode roundtrip
if let Some(encoded) = idna::punycode::encode_str(utf8) {
if let Some(decoded) = idna::punycode::decode_to_string(&encoded) {
assert_eq!(
utf8, decoded,
"Punycode roundtrip mismatch: input={:?}, encoded={:?}, decoded={:?}",
utf8, encoded, decoded
);
}
}
});
Loading
Loading