Author SHA1 Message Date
Claude bd0a0f0c26 Add comprehensive fuzzing infrastructure for URL parser
This commit adds AFL++ (American Fuzzy Lop) fuzzing support to help find bugs,
crashes, and security vulnerabilities in the URL parser.

Changes:
- Add fuzz_afl.c: AFL++ fuzz target that tests all parser functions
  - url_parse() with different flag combinations
  - url_parse_ipv4() and url_parse_ipv6()
  - url_serialize(), url_percent_decode(), url_remove_white_space()

- Add build_afl.sh: Build script with AFL++ compiler detection
- Add run_afl.sh: Automated fuzzing script with result reporting
- Add url.dict: Dictionary file to guide AFL++ mutations

- Add corpus/: Seed inputs covering various URL patterns
  - Basic URLs, IPv4/IPv6, percent-encoding, relative paths
  - Edge cases and special schemes (file://, mailto:, etc.)

- Add FUZZING.md: Comprehensive documentation
  - Installation instructions
  - Quick start guide
  - Advanced usage (parallel fuzzing, sanitizers)
  - Troubleshooting tips

- Add fuzz.c: Standalone mutation-based fuzzer (no AFL++ required)
  - Useful for quick testing without installing AFL++
  - Implements various mutation strategies
2025-12-06 15:23:32 +00:00
21 changed files with 960 additions and 58 deletions
+4 -2
View File
@@ -1,5 +1,5 @@
# url.c
This is a small library to parse and manipulate URLs in conformance to RFC 3986 and (most of) the WHATWG specification.
This is a small library to parse and manipulate URLs in conformance to RFC 3986 and WHATWG.
It features
* No allocations
@@ -15,4 +15,6 @@ You just copy paste `url.h` and `url.c` in your source tree and compile them as
Some example programs are provided in the `examples/` folder. To compile them, run `build.bat` on Windows or `build.sh` on Linux.
# Testing
The parser and reference resolver is tested against the URL section of the [web platform tests](https://github.com/web-platform-tests/wpt), which validates implementations that adhere to the WHATWG specification (what the major browsers actually do). To run the test suite, `cd` into `tests/` and compile the test runner using `build.sh` (Linux) or `build.bat` (Windows). Then, run `./test.out` (Linux) or `./test.exe` (Windows). Note that not all tests pass, which is to be expected as adherence to the WHATWG specification is a work-in-progress.
The parser and reference resolver is tested against the URL section of the [web platform tests](https://github.com/web-platform-tests/wpt). Note that not all tests pass. This is due to the parser not doing any normalization and not supporting more niche features such as converting `\` characters to `/` or supporting drive letters like `C:` in the URL's path.
To run the test suite, `cd` into `tests/` and compile the test runner using `build.sh` (Linux) or `build.bat` (Windows). Then, run `./test.out` (Linux) or `./test.exe` (Windows).
+228
View File
@@ -0,0 +1,228 @@
# Fuzzing the URL Parser
This directory contains fuzzing infrastructure for the URL parser using AFL++ (American Fuzzy Lop).
## What is Fuzzing?
Fuzzing is an automated testing technique that feeds malformed, unexpected, or random data to a program to find bugs, crashes, and security vulnerabilities. AFL++ is a coverage-guided fuzzer that intelligently mutates inputs to maximize code coverage.
## Prerequisites
### Install AFL++
**Ubuntu/Debian:**
```bash
sudo apt-get update
sudo apt-get install afl++
```
**From source:**
```bash
git clone https://github.com/AFLplusplus/AFLplusplus
cd AFLplusplus
make
sudo make install
```
## Quick Start
1. **Build the fuzz target:**
```bash
cd tests/
./build_afl.sh
```
2. **Run fuzzing:**
```bash
./run_afl.sh
```
Or run for a specific duration:
```bash
./run_afl.sh 60 # Run for 60 minutes
```
3. **View results:**
- Crashes: `findings/default/crashes/`
- Hangs: `findings/default/hangs/`
- Interesting inputs: `findings/default/queue/`
## Directory Structure
```
tests/
├── fuzz_afl.c # AFL++ fuzz target
├── build_afl.sh # Build script
├── run_afl.sh # Run script
├── corpus/ # Seed inputs
│ ├── basic.txt
│ ├── complex.txt
│ ├── ipv4.txt
│ ├── ipv6.txt
│ └── ...
└── findings/ # AFL++ output (created when running)
└── default/
├── crashes/ # Inputs that caused crashes
├── hangs/ # Inputs that caused timeouts
└── queue/ # All interesting inputs found
```
## What Gets Tested
The fuzz target tests multiple parser functions:
- `url_parse()` - Main URL parser (with different flag combinations)
- `url_parse_ipv4()` - IPv4 address parser
- `url_parse_ipv6()` - IPv6 address parser
- `url_serialize()` - URL serialization
- `url_percent_decode()` - Percent-decoding
- `url_remove_white_space()` - Whitespace removal
## Analyzing Results
### Reproducing Crashes
If AFL++ finds a crash, you can reproduce it:
```bash
./fuzz_afl < findings/default/crashes/id:000000,*
```
Or use a debugger:
```bash
gdb ./fuzz_afl
(gdb) run < findings/default/crashes/id:000000,*
```
### Minimizing Crash Inputs
AFL++ can minimize crash inputs to their smallest form:
```bash
afl-tmin -i findings/default/crashes/id:000000,* -o minimized.txt -- ./fuzz_afl
```
### Viewing Statistics
While fuzzing is running, AFL++ displays real-time statistics:
- **Cycles done**: Number of times the fuzzer went through the queue
- **Corpus count**: Number of interesting test cases found
- **Crashes**: Number of unique crashes found
- **Exec speed**: Fuzzing throughput (executions per second)
## Advanced Usage
### Parallel Fuzzing
Run multiple AFL++ instances in parallel for better coverage:
```bash
# Terminal 1 (master)
afl-fuzz -i corpus/ -o findings/ -M fuzzer1 -- ./fuzz_afl
# Terminal 2 (secondary)
afl-fuzz -i corpus/ -o findings/ -S fuzzer2 -- ./fuzz_afl
# Terminal 3 (secondary)
afl-fuzz -i corpus/ -o findings/ -S fuzzer3 -- ./fuzz_afl
```
### Using Different Modes
AFL++ supports various mutation modes:
```bash
# Explore mode (default)
afl-fuzz -i corpus/ -o findings/ -- ./fuzz_afl
# Exploitation mode (focus on found crashes)
afl-fuzz -i corpus/ -o findings/ -p exploit -- ./fuzz_afl
# COE (Continue on Error) mode
afl-fuzz -i corpus/ -o findings/ -c -- ./fuzz_afl
```
### With AddressSanitizer (ASAN)
For better bug detection, rebuild with AddressSanitizer:
```bash
AFL_USE_ASAN=1 ./build_afl.sh
./run_afl.sh
```
Note: ASAN significantly slows down fuzzing but can detect memory issues that wouldn't cause crashes.
### With UndefinedBehaviorSanitizer (UBSAN)
To detect undefined behavior:
```bash
AFL_USE_UBSAN=1 ./build_afl.sh
./run_afl.sh
```
## Performance Tips
1. **Disable CPU frequency scaling:**
```bash
echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
```
2. **Increase system limits:**
```bash
sudo sysctl -w kernel.core_pattern=core
sudo sysctl -w kernel.sched_child_runs_first=1
```
3. **Use persistent mode** (requires code changes to fuzz target)
4. **Use dictionary files** for better mutations:
```bash
afl-fuzz -i corpus/ -o findings/ -x url.dict -- ./fuzz_afl
```
## Continuous Fuzzing
For long-term fuzzing campaigns:
```bash
# Run in screen/tmux
screen -S fuzzing
./run_afl.sh
# Detach: Ctrl+A, D
# Reattach: screen -r fuzzing
```
Or use a systemd service for continuous fuzzing.
## Troubleshooting
### "No instrumentation detected"
Make sure you built with an AFL++ compiler (afl-gcc, afl-clang-fast, etc.)
### "Suboptimal CPU scaling governor"
Run as suggested by AFL++ or set `AFL_SKIP_CPUFREQ=1`
### Low execution speed
- Close other programs
- Use afl-clang-fast instead of afl-gcc
- Consider using QEMU mode for uninstrumented binaries
### No new paths found
- Add more diverse seed inputs to corpus/
- Try different mutation strategies
- Consider parallel fuzzing
## References
- [AFL++ Documentation](https://github.com/AFLplusplus/AFLplusplus/tree/stable/docs)
- [Fuzzing Tutorial](https://github.com/google/fuzzing/blob/master/tutorial/libFuzzerTutorial.md)
- [OWASP Fuzzing Guide](https://owasp.org/www-community/Fuzzing)
+44
View File
@@ -0,0 +1,44 @@
#!/bin/bash
# Build script for AFL++ fuzzing
#
# Prerequisites:
# - AFL++ installed (https://github.com/AFLplusplus/AFLplusplus)
#
# Install AFL++ on Ubuntu/Debian:
# sudo apt-get install afl++
#
# Or build from source:
# git clone https://github.com/AFLplusplus/AFLplusplus
# cd AFLplusplus
# make
# sudo make install
set -e
echo "Building fuzz target with AFL++..."
# Try different AFL++ compilers in order of preference
if command -v afl-clang-fast &> /dev/null; then
echo "Using afl-clang-fast (recommended)"
AFL_CC=afl-clang-fast
elif command -v afl-gcc &> /dev/null; then
echo "Using afl-gcc"
AFL_CC=afl-gcc
elif command -v afl-clang &> /dev/null; then
echo "Using afl-clang"
AFL_CC=afl-clang
else
echo "Error: No AFL++ compiler found!"
echo "Please install AFL++ first:"
echo " sudo apt-get install afl++"
echo "Or see: https://github.com/AFLplusplus/AFLplusplus"
exit 1
fi
# Build the fuzz target
$AFL_CC -o fuzz_afl fuzz_afl.c ../url.c -Wall -Wextra -ggdb
echo "Build successful! Binary: fuzz_afl"
echo ""
echo "To run fuzzing:"
echo " ./run_afl.sh"
+1
View File
@@ -0,0 +1 @@
gcc -o fuzz.exe fuzz.c ../url.c -Wall -Wextra -ggdb -O2
+2
View File
@@ -0,0 +1,2 @@
#!/bin/bash
gcc -o fuzz.out fuzz.c ../url.c -Wall -Wextra -ggdb -O2
+1
View File
@@ -0,0 +1 @@
//example.com/path
+1
View File
@@ -0,0 +1 @@
http://example.com
+1
View File
@@ -0,0 +1 @@
https://user:pass@example.com:8080/path?query=value#fragment
+1
View File
@@ -0,0 +1 @@
http://example.com/path%20with%20spaces
+1
View File
@@ -0,0 +1 @@
file:///path/to/file
+1
View File
@@ -0,0 +1 @@
http://192.168.1.1/path
+1
View File
@@ -0,0 +1 @@
http://[2001:db8::1]/
+1
View File
@@ -0,0 +1 @@
http://localhost:3000/api/v1
+1
View File
@@ -0,0 +1 @@
mailto:user@example.com
+1
View File
@@ -0,0 +1 @@
../relative/path
+363
View File
@@ -0,0 +1,363 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <time.h>
#include <stdbool.h>
#include "../url.h"
// PRNG state
static uint64_t rng_state = 0;
// Simple xorshift64 RNG
static uint64_t rand64(void)
{
rng_state ^= rng_state << 13;
rng_state ^= rng_state >> 7;
rng_state ^= rng_state << 17;
return rng_state;
}
// Generate random byte
static uint8_t rand_byte(void)
{
return (uint8_t)(rand64() & 0xFF);
}
// Generate random integer in range [0, max)
static int rand_range(int max)
{
if (max <= 0) return 0;
return (int)(rand64() % max);
}
// Seed URLs for mutation-based fuzzing
static const char *seed_urls[] = {
"http://example.com",
"https://user:pass@example.com:8080/path?query=value#fragment",
"ftp://192.168.1.1/file.txt",
"http://[2001:db8::1]/",
"mailto:user@example.com",
"file:///path/to/file",
"http://example.com:80/path",
"https://example.com/path%20with%20spaces",
"http://localhost:3000",
"http://127.0.0.1:8080/api/v1/resource",
"//example.com/path",
"/path/to/resource",
"../relative/path",
"?query=only",
"#fragment-only",
"http://example.com:99999",
"http://256.256.256.256",
"http://example..com",
"http://:password@example.com",
"http://user:@example.com",
"http://example.com/path//double//slash",
"http://example.com/path/../../../etc/passwd",
"http://example.com/%2e%2e%2f",
"http://example.com/\x00null",
"http://example.com/\x0a\x0d",
"http://\t\n\rexample.com",
"http://example.com:0",
"http://example.com:-1",
"http://example.com:abc",
"http://[::1]",
"http://[::ffff:192.0.2.1]",
"http://[2001:db8::1]:8080",
"http://[::1",
"http://::1]",
"data:text/plain;base64,SGVsbG8=",
"javascript:alert(1)",
"about:blank",
};
#define NUM_SEEDS (sizeof(seed_urls) / sizeof(seed_urls[0]))
// Character classes for generation
static const char *char_classes[] = {
"abcdefghijklmnopqrstuvwxyz",
"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
"0123456789",
"://",
"@.:/?#[]",
"%",
"\t\n\r ",
"\x00\x01\x02\x03\x04\x05\x06\x07\x08",
};
#define NUM_CHAR_CLASSES (sizeof(char_classes) / sizeof(char_classes[0]))
// Mutation strategies
typedef enum {
MUT_BIT_FLIP,
MUT_BYTE_FLIP,
MUT_BYTE_INSERT,
MUT_BYTE_DELETE,
MUT_BYTE_REPLACE,
MUT_CHUNK_INSERT,
MUT_CHUNK_DELETE,
MUT_CHUNK_DUPLICATE,
MUT_INTERESTING_VALUE,
MUT_COUNT
} MutationType;
// Interesting byte values
static const uint8_t interesting_bytes[] = {
0x00, 0x01, 0xFF, 0x7F, 0x80,
'/', '?', '#', ':', '@', '[', ']',
'%', '.', '-', '_', '~',
};
// Mutate a buffer
static int mutate_buffer(char *dst, int dst_cap, const char *src, int src_len)
{
if (src_len <= 0 || src_len >= dst_cap) {
// Can't mutate empty or too large buffer
return src_len;
}
// Copy source to destination
memcpy(dst, src, src_len);
int len = src_len;
// Apply 1-5 random mutations
int num_mutations = 1 + rand_range(5);
for (int i = 0; i < num_mutations && len > 0 && len < dst_cap - 1; i++) {
MutationType mut = rand_range(MUT_COUNT);
int pos = rand_range(len);
switch (mut) {
case MUT_BIT_FLIP:
if (pos < len) {
dst[pos] ^= (1 << rand_range(8));
}
break;
case MUT_BYTE_FLIP:
if (pos < len) {
dst[pos] ^= 0xFF;
}
break;
case MUT_BYTE_INSERT:
if (len < dst_cap - 1) {
memmove(dst + pos + 1, dst + pos, len - pos);
dst[pos] = rand_byte();
len++;
}
break;
case MUT_BYTE_DELETE:
if (pos < len && len > 1) {
memmove(dst + pos, dst + pos + 1, len - pos - 1);
len--;
}
break;
case MUT_BYTE_REPLACE:
if (pos < len) {
dst[pos] = rand_byte();
}
break;
case MUT_CHUNK_INSERT: {
int chunk_len = 1 + rand_range(16);
if (len + chunk_len < dst_cap) {
memmove(dst + pos + chunk_len, dst + pos, len - pos);
for (int j = 0; j < chunk_len; j++) {
dst[pos + j] = rand_byte();
}
len += chunk_len;
}
break;
}
case MUT_CHUNK_DELETE: {
int chunk_len = 1 + rand_range(16);
if (pos + chunk_len <= len) {
memmove(dst + pos, dst + pos + chunk_len, len - pos - chunk_len);
len -= chunk_len;
}
break;
}
case MUT_CHUNK_DUPLICATE: {
int chunk_len = 1 + rand_range(16);
if (pos + chunk_len <= len && len + chunk_len < dst_cap) {
memmove(dst + pos + chunk_len, dst + pos, len - pos);
memcpy(dst + pos + chunk_len, dst + pos, chunk_len);
len += chunk_len;
}
break;
}
case MUT_INTERESTING_VALUE:
if (pos < len) {
dst[pos] = interesting_bytes[rand_range(sizeof(interesting_bytes))];
}
break;
default:
break;
}
}
return len;
}
// Generate a random URL-like string
static int generate_random_url(char *buf, int cap)
{
int strategy = rand_range(3);
if (strategy == 0) {
// Fully random generation
int len = rand_range(cap - 1);
for (int i = 0; i < len; i++) {
buf[i] = rand_byte();
}
return len;
} else if (strategy == 1) {
// Generate from character classes
int len = rand_range(cap - 1);
for (int i = 0; i < len; i++) {
const char *class = char_classes[rand_range(NUM_CHAR_CLASSES)];
int class_len = strlen(class);
buf[i] = class[rand_range(class_len)];
}
return len;
} else {
// Mutation-based: pick a seed and mutate it
const char *seed = seed_urls[rand_range(NUM_SEEDS)];
int seed_len = strlen(seed);
return mutate_buffer(buf, cap, seed, seed_len);
}
}
// Fuzz url_parse
static void fuzz_url_parse(const char *input, int len)
{
URL url;
int flags_options[] = { 0, URL_FLAG_ALLOWREF, URL_FLAG_RFC3986, URL_FLAG_ALLOWREF | URL_FLAG_RFC3986 };
int flags = flags_options[rand_range(4)];
// Test with NULL cursor
url_parse((char *)input, len, NULL, &url, flags);
// Test with cursor
int cursor = 0;
url_parse((char *)input, len, &cursor, &url, flags);
// Test serialization if parse succeeded
if (url_parse((char *)input, len, NULL, &url, flags) == 0) {
char output[4096];
url_serialize(url, NULL, output, sizeof(output));
}
// Test percent decode on random substrings
if (len > 0) {
int start = rand_range(len);
int end = start + rand_range(len - start);
URL_String str = { (char *)input + start, end - start };
char decoded[4096];
url_percent_decode(str, decoded, sizeof(decoded));
}
// Test whitespace removal
char tmp[4096];
url_remove_white_space((char *)input, len, tmp, sizeof(tmp));
}
// Fuzz url_parse_ipv4
static void fuzz_ipv4_parse(const char *input, int len)
{
URL_IPv4 ipv4;
// Test with NULL cursor
url_parse_ipv4((char *)input, len, NULL, &ipv4);
// Test with cursor
int cursor = 0;
url_parse_ipv4((char *)input, len, &cursor, &ipv4);
}
// Fuzz url_parse_ipv6
static void fuzz_ipv6_parse(const char *input, int len)
{
URL_IPv6 ipv6;
// Test with NULL cursor
url_parse_ipv6((char *)input, len, NULL, &ipv6);
// Test with cursor
int cursor = 0;
url_parse_ipv6((char *)input, len, &cursor, &ipv6);
}
// Run one fuzz iteration
static void fuzz_iteration(int iteration)
{
char input[4096];
int len = generate_random_url(input, sizeof(input));
// Ensure null termination for safety (though our API doesn't require it)
if (len < (int)sizeof(input)) {
input[len] = '\0';
}
// Fuzz different functions
int target = rand_range(3);
switch (target) {
case 0:
fuzz_url_parse(input, len);
break;
case 1:
fuzz_ipv4_parse(input, len);
break;
case 2:
fuzz_ipv6_parse(input, len);
break;
}
}
int main(int argc, char **argv)
{
// Initialize RNG
if (argc > 1) {
rng_state = strtoull(argv[1], NULL, 10);
} else {
rng_state = time(NULL);
}
printf("URL Parser Fuzzer\n");
printf("Seed: %llu\n", (unsigned long long)rng_state);
printf("Running fuzz tests...\n\n");
int num_iterations = 100000;
if (argc > 2) {
num_iterations = atoi(argv[2]);
}
clock_t start = clock();
for (int i = 0; i < num_iterations; i++) {
fuzz_iteration(i);
// Progress update every 10000 iterations
if (i > 0 && i % 10000 == 0) {
double elapsed = (double)(clock() - start) / CLOCKS_PER_SEC;
double rate = i / elapsed;
printf("\rIteration %d/%d (%.0f iter/sec)", i, num_iterations, rate);
fflush(stdout);
}
}
double elapsed = (double)(clock() - start) / CLOCKS_PER_SEC;
printf("\n\nCompleted %d iterations in %.2f seconds\n", num_iterations, elapsed);
printf("Average rate: %.0f iterations/second\n", num_iterations / elapsed);
printf("\nNo crashes detected!\n");
return 0;
}
+115
View File
@@ -0,0 +1,115 @@
/*
* AFL++ fuzz target for url.c parser
*
* Build with AFL++:
* afl-gcc -o fuzz_afl fuzz_afl.c ../url.c -Wall -Wextra -ggdb
*
* Or with afl-clang-fast for better instrumentation:
* afl-clang-fast -o fuzz_afl fuzz_afl.c ../url.c -Wall -Wextra -ggdb
*
* Run with:
* afl-fuzz -i corpus/ -o findings/ -- ./fuzz_afl
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include "../url.h"
// Maximum input size
#define MAX_INPUT_SIZE 8192
// Test buffer for outputs
static char output_buffer[16384];
int main(int argc, char **argv)
{
(void)argc;
(void)argv;
// Read input from stdin (AFL++ feeds input this way)
unsigned char input[MAX_INPUT_SIZE];
ssize_t len = read(STDIN_FILENO, input, sizeof(input));
if (len <= 0) {
return 0;
}
// Ensure null termination for safety
if (len < (ssize_t)sizeof(input)) {
input[len] = '\0';
}
// Fuzz url_parse with different flag combinations
URL url;
int flags_to_test[] = {
0,
URL_FLAG_ALLOWREF,
URL_FLAG_RFC3986,
URL_FLAG_ALLOWREF | URL_FLAG_RFC3986
};
for (int i = 0; i < 4; i++) {
int flags = flags_to_test[i];
// Test with NULL cursor
int ret = url_parse((char *)input, len, NULL, &url, flags);
// Test with cursor
int cursor = 0;
url_parse((char *)input, len, &cursor, &url, flags);
// If parse succeeded, test serialization
if (ret == 0) {
url_serialize(url, NULL, output_buffer, sizeof(output_buffer));
// Test serialization with itself as base
url_serialize(url, &url, output_buffer, sizeof(output_buffer));
}
}
// Test whitespace removal
int cleaned_len = url_remove_white_space((char *)input, len, output_buffer, sizeof(output_buffer));
// If whitespace removal succeeded, try parsing the cleaned input
if (cleaned_len > 0 && cleaned_len < (int)sizeof(output_buffer)) {
url_parse(output_buffer, cleaned_len, NULL, &url, 0);
}
// Test percent decode on the full input
URL_String str = { (char *)input, len };
url_percent_decode(str, output_buffer, sizeof(output_buffer));
// Test IPv4 parsing
URL_IPv4 ipv4;
url_parse_ipv4((char *)input, len, NULL, &ipv4);
int cursor = 0;
url_parse_ipv4((char *)input, len, &cursor, &ipv4);
// Test IPv6 parsing
URL_IPv6 ipv6;
url_parse_ipv6((char *)input, len, NULL, &ipv6);
cursor = 0;
url_parse_ipv6((char *)input, len, &cursor, &ipv6);
// Test parsing with base URL (if input is long enough)
if (len > 10) {
// Try to use first half as base, second half as reference
int mid = len / 2;
URL base_url;
if (url_parse((char *)input, mid, NULL, &base_url, 0) == 0) {
URL reference;
if (url_parse((char *)input + mid, len - mid, NULL, &reference, URL_FLAG_ALLOWREF) == 0) {
url_serialize(reference, &base_url, output_buffer, sizeof(output_buffer));
}
}
}
return 0;
}
+86
View File
@@ -0,0 +1,86 @@
#!/bin/bash
# Run AFL++ fuzzing
#
# Usage:
# ./run_afl.sh [duration_in_minutes]
#
# Examples:
# ./run_afl.sh # Run indefinitely (press Ctrl+C to stop)
# ./run_afl.sh 60 # Run for 60 minutes
set -e
# Check if fuzz target exists
if [ ! -f "fuzz_afl" ]; then
echo "Error: fuzz_afl binary not found!"
echo "Please run ./build_afl.sh first"
exit 1
fi
# Check if AFL++ is installed
if ! command -v afl-fuzz &> /dev/null; then
echo "Error: afl-fuzz not found!"
echo "Please install AFL++:"
echo " sudo apt-get install afl++"
exit 1
fi
# Create output directory if it doesn't exist
mkdir -p findings
# Set AFL++ options for better performance
export AFL_SKIP_CPUFREQ=1
export AFL_I_DONT_CARE_ABOUT_MISSING_CRASHES=1
echo "Starting AFL++ fuzzing..."
echo "Corpus directory: corpus/"
echo "Output directory: findings/"
echo ""
echo "Press Ctrl+C to stop fuzzing"
echo ""
# Run AFL++ with optional timeout and dictionary
DICT_ARG=""
if [ -f "url.dict" ]; then
DICT_ARG="-x url.dict"
echo "Using dictionary: url.dict"
fi
if [ -n "$1" ]; then
timeout_sec=$((60 * $1))
echo "Running for $1 minutes ($timeout_sec seconds)"
timeout $timeout_sec afl-fuzz -i corpus/ -o findings/ $DICT_ARG -- ./fuzz_afl || true
else
afl-fuzz -i corpus/ -o findings/ $DICT_ARG -- ./fuzz_afl
fi
echo ""
echo "Fuzzing completed!"
echo ""
echo "Results:"
echo " Crashes: findings/default/crashes/"
echo " Hangs: findings/default/hangs/"
echo " Queue: findings/default/queue/"
echo ""
# Check for crashes
if [ -d "findings/default/crashes" ]; then
num_crashes=$(find findings/default/crashes -type f ! -name 'README.txt' | wc -l)
if [ $num_crashes -gt 0 ]; then
echo "⚠️ Found $num_crashes crash(es)!"
echo "To reproduce a crash:"
echo " ./fuzz_afl < findings/default/crashes/id:000000,*"
else
echo "✓ No crashes found"
fi
fi
# Check for hangs
if [ -d "findings/default/hangs" ]; then
num_hangs=$(find findings/default/hangs -type f ! -name 'README.txt' | wc -l)
if [ $num_hangs -gt 0 ]; then
echo "⚠️ Found $num_hangs hang(s)!"
else
echo "✓ No hangs found"
fi
fi
+8 -34
View File
@@ -31,10 +31,6 @@ void test_printf(char *fmt, ...)
sizeof(test_output_buffer) - test_output_buffer_used,
fmt, args);
va_end(args);
assert(len > -1);
if (len > (int) sizeof(test_output_buffer) - test_output_buffer_used)
len = sizeof(test_output_buffer) - test_output_buffer_used;
test_output_buffer_used += len;
}
@@ -202,21 +198,15 @@ static int json_to_url(cJSON *json, URL *url)
url->fragment.len--;
}
if (port.len == 0) {
url->no_port = 1;
url->port = 0;
} else {
char tmp[8];
if (port.len >= (int) sizeof(tmp)) {
test_printf(" JSON object's port field is longer than expected\n");
return -1;
}
memcpy(tmp, port.ptr, port.len);
tmp[port.len] = 0;
url->no_port = 0;
url->port = atoi(tmp);
char tmp[8];
if (port.len >= (int) sizeof(tmp)) {
test_printf(" JSON object's port field is longer than expected\n");
return -1;
}
memcpy(tmp, port.ptr, port.len);
tmp[port.len] = 0;
url->port = atoi(tmp);
return 0;
}
@@ -275,20 +265,6 @@ static int compare_urls(URL parsed, URL expected)
ok = false;
}
if (parsed.no_port != expected.no_port) {
if (ok) test_printf("\n");
if (parsed.no_port) {
test_printf(" port mismatch\n");
test_printf(" parsed (empty)\n");
test_printf(" expected [%d]\n", expected.port);
} else {
test_printf(" port mismatch\n");
test_printf(" parsed [%d]\n", parsed.port);
test_printf(" expected (empty)\n");
}
ok = false;
}
if (parsed.port != expected.port) {
if (ok) test_printf("\n");
test_printf(" port mismatch\n");
@@ -512,7 +488,6 @@ int main(void)
if (!cJSON_IsArray(root)) {
fprintf(stderr, "Error: %s doesn't contain a JSON array as expected", path);
cJSON_Delete(root);
free(buf);
return -1;
}
@@ -565,7 +540,6 @@ int main(void)
printf(" failed : %d\n", num_failed);
printf(" aborted: %d\n", num_aborted);
cJSON_Delete(root);
free(buf);
return 0;
}
+84
View File
@@ -0,0 +1,84 @@
# AFL++ dictionary for URL fuzzing
# This helps AFL++ generate more interesting mutations
# URL schemes
"http://"
"https://"
"ftp://"
"file://"
"mailto:"
"data:"
"ws://"
"wss://"
# Special characters
":"
"/"
"?"
"#"
"@"
"."
"%"
"["
"]"
# Authority separators
"//"
"@"
# Port separators
":80"
":443"
":8080"
":3000"
# Percent encoding
"%20"
"%2F"
"%3A"
"%00"
"%FF"
# IPv4 patterns
"127.0.0.1"
"192.168.1.1"
"0.0.0.0"
"255.255.255.255"
# IPv6 patterns
"::1"
"::"
"2001:db8::1"
"::ffff:192.0.2.1"
# Common paths
"/index.html"
"/api/v1"
"/../"
"/.."
"/."
# Common query strings
"?query=value"
"&"
"="
# Common fragments
"#fragment"
"#top"
# Userinfo
"user:password@"
"user@"
":password@"
# Special values
"localhost"
"example.com"
"example.org"
# Whitespace
"\t"
"\n"
"\r"
" "
+15 -22
View File
@@ -580,26 +580,25 @@ int url_parse(char *src, int len, int *pcur, URL *out, int flags)
out->no_port = 1;
out->port = 0;
if (cur < len && src[cur] == ':') {
// The WHATWG standard forbids port numbers with the
// "file" protocol.
if ((flags & URL_FLAG_RFC3986) == 0) {
if (streqcase(out->scheme, S("file")))
return -1;
}
cur++; // Consume the ':'
out->no_port = 0;
if (cur < len && is_digit(src[cur])) {
while (cur < len && is_digit(src[cur])) {
// The WHATWG standard forbids port numbers with the
// "file" protocol.
if ((flags & URL_FLAG_RFC3986) == 0) {
if (streqcase(out->scheme, S("file")))
return -1;
}
int n = src[cur] - '0';
cur++;
out->no_port = 0;
do {
int n = src[cur] - '0';
cur++;
if (out->port > (UINT16_MAX - n) / 10)
return -1; // Overflow
out->port = out->port * 10 + n;
} while (cur < len && is_digit(src[cur]));
if (out->port > (UINT16_MAX - n) / 10)
return -1; // Overflow
out->port = out->port * 10 + n;
}
}
@@ -857,8 +856,6 @@ static void append_fragment(Builder *b, URL_String fragment)
int url_serialize(URL url, URL *base, char *dst, int cap)
{
ASSERT(cap == 0 || dst != NULL);
if (base != NULL && base->scheme.len == 0)
return -1; // Base is not an absolute URL
@@ -962,8 +959,6 @@ static URL_b32 is_white_space(char c)
int url_remove_white_space(char *src, int len, char *dst, int cap)
{
ASSERT(cap == 0 || dst != NULL);
while (len > 0 && is_white_space(src[0])) {
src++;
len--;
@@ -986,8 +981,6 @@ int url_remove_white_space(char *src, int len, char *dst, int cap)
int url_percent_decode(URL_String str, char *dst, int cap)
{
ASSERT(cap == 0 || dst != NULL);
char *src = str.ptr;
int len = str.len;
int rd = 0;