Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4871f5168d |
@@ -2,3 +2,4 @@ gcc -o 000_parse.exe examples/000_parse.c url.c -I. -Wall -Wextr
|
|||||||
gcc -o 001_serialize.exe examples/001_serialize.c url.c -I. -Wall -Wextra -ggdb
|
gcc -o 001_serialize.exe examples/001_serialize.c url.c -I. -Wall -Wextra -ggdb
|
||||||
gcc -o 002_partial_parse.exe examples/002_partial_parse.c url.c -I. -Wall -Wextra -ggdb
|
gcc -o 002_partial_parse.exe examples/002_partial_parse.c url.c -I. -Wall -Wextra -ggdb
|
||||||
gcc -o 003_references.exe examples/003_references.c url.c -I. -Wall -Wextra -ggdb
|
gcc -o 003_references.exe examples/003_references.c url.c -I. -Wall -Wextra -ggdb
|
||||||
|
gcc -o 004_build.exe examples/004_build.c url.c -I. -Wall -Wextra -ggdb
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ gcc -o 000_parse.out examples/000_parse.c url.c -I. -Wall -Wextr
|
|||||||
gcc -o 001_serialize.out examples/001_serialize.c url.c -I. -Wall -Wextra -ggdb
|
gcc -o 001_serialize.out examples/001_serialize.c url.c -I. -Wall -Wextra -ggdb
|
||||||
gcc -o 002_partial_parse.out examples/002_partial_parse.c url.c -I. -Wall -Wextra -ggdb
|
gcc -o 002_partial_parse.out examples/002_partial_parse.c url.c -I. -Wall -Wextra -ggdb
|
||||||
gcc -o 003_references.out examples/003_references.c url.c -I. -Wall -Wextra -ggdb
|
gcc -o 003_references.out examples/003_references.c url.c -I. -Wall -Wextra -ggdb
|
||||||
|
gcc -o 004_build.out examples/004_build.c url.c -I. -Wall -Wextra -ggdb
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#include <url.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
URL_Builder builder;
|
||||||
|
url_builder_init(&builder);
|
||||||
|
|
||||||
|
url_builder_set_scheme(&builder, "http", -1);
|
||||||
|
|
||||||
|
url_builder_set_username(&builder, "cozis", -1);
|
||||||
|
|
||||||
|
url_builder_set_password(&builder, "my_secret", -1);
|
||||||
|
|
||||||
|
url_builder_set_host_name(&builder, "www.ex@mple.com", -1);
|
||||||
|
|
||||||
|
url_builder_set_path(&builder, "/../../index.html", -1);
|
||||||
|
|
||||||
|
char url[1<<9];
|
||||||
|
int len = url_builder_finalize(&builder, url, sizeof(url));
|
||||||
|
if (len < 0) {
|
||||||
|
printf("Couldn't build URL\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (len >= (int) sizeof(url)) {
|
||||||
|
printf("URL buffer is too small\n");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
url[len] = '\0';
|
||||||
|
|
||||||
|
printf("Built URL: %s\n", url);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
# 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)
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
#!/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 +0,0 @@
|
|||||||
gcc -o fuzz.exe fuzz.c ../url.c -Wall -Wextra -ggdb -O2
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
gcc -o fuzz.out fuzz.c ../url.c -Wall -Wextra -ggdb -O2
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
//example.com/path
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
http://example.com
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
https://user:pass@example.com:8080/path?query=value#fragment
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
http://example.com/path%20with%20spaces
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
file:///path/to/file
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
http://192.168.1.1/path
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
http://[2001:db8::1]/
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
http://localhost:3000/api/v1
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
mailto:user@example.com
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
../relative/path
|
|
||||||
-363
@@ -1,363 +0,0 @@
|
|||||||
#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;
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
#!/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
|
|
||||||
+26
-42
@@ -217,89 +217,73 @@ static bool streq(URL_String a, URL_String b)
|
|||||||
return !memcmp(a.ptr, b.ptr, a.len);
|
return !memcmp(a.ptr, b.ptr, a.len);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int compare_urls(URL parsed, URL expected)
|
static int compare_urls(URL url_1, URL url_2, char *label_1, char *label_2)
|
||||||
{
|
{
|
||||||
#define UNPACK(s) (s).len, (s).ptr
|
#define UNPACK(s) (s).len, (s).ptr
|
||||||
|
|
||||||
bool ok = true;
|
bool ok = true;
|
||||||
|
|
||||||
if (!streq(parsed.scheme, expected.scheme)) {
|
if (!streq(url_1.scheme, url_2.scheme)) {
|
||||||
if (ok) test_printf("\n");
|
if (ok) test_printf("\n");
|
||||||
test_printf(" scheme mismatch\n");
|
test_printf(" scheme mismatch\n");
|
||||||
test_printf(" parsed [%.*s]\n", UNPACK(parsed.scheme));
|
test_printf(" %s [%.*s]\n", label_1, UNPACK(url_1.scheme));
|
||||||
test_printf(" expected [%.*s]\n", UNPACK(expected.scheme));
|
test_printf(" %s [%.*s]\n", label_2, UNPACK(url_2.scheme));
|
||||||
ok = false;
|
ok = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
char tmp[1<<9];
|
if (!streq(url_1.username, url_2.username)) {
|
||||||
int ret = url_percent_decode(parsed.username, tmp, sizeof(tmp));
|
|
||||||
assert(ret > -1 && ret < (int) sizeof(tmp));
|
|
||||||
|
|
||||||
if (!streq((URL_String) { tmp, ret }, expected.username)) {
|
|
||||||
if (ok) test_printf("\n");
|
if (ok) test_printf("\n");
|
||||||
test_printf(" username mismatch\n");
|
test_printf(" username mismatch\n");
|
||||||
test_printf(" parsed [%.*s]\n", ret, tmp);
|
test_printf(" %s [%.*s]\n", label_1, UNPACK(url_1.username));
|
||||||
test_printf(" expected [%.*s]\n", UNPACK(expected.username));
|
test_printf(" %s [%.*s]\n", label_2, UNPACK(url_2.username));
|
||||||
ok = false;
|
ok = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = url_percent_decode(parsed.password, tmp, sizeof(tmp));
|
if (!streq(url_1.password, url_2.password)) {
|
||||||
assert(ret > -1 && ret < (int) sizeof(tmp));
|
|
||||||
|
|
||||||
if (!streq((URL_String) { tmp, ret }, expected.password)) {
|
|
||||||
if (ok) test_printf("\n");
|
if (ok) test_printf("\n");
|
||||||
test_printf(" password mismatch\n");
|
test_printf(" password mismatch\n");
|
||||||
test_printf(" parsed [%.*s]\n", ret, tmp);
|
test_printf(" %s [%.*s]\n", label_1, UNPACK(url_1.password));
|
||||||
test_printf(" expected [%.*s]\n", UNPACK(expected.password));
|
test_printf(" %s [%.*s]\n", label_2, UNPACK(url_2.password));
|
||||||
ok = false;
|
ok = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = url_percent_decode(parsed.host_text, tmp, sizeof(tmp));
|
if (!streq(url_1.host_text, url_2.host_text)) {
|
||||||
assert(ret > -1 && ret < (int) sizeof(tmp));
|
|
||||||
|
|
||||||
if (!streq((URL_String) { tmp, ret }, expected.host_text)) {
|
|
||||||
if (ok) test_printf("\n");
|
if (ok) test_printf("\n");
|
||||||
test_printf(" host mismatch\n");
|
test_printf(" host mismatch\n");
|
||||||
test_printf(" parsed [%.*s]\n", ret, tmp);
|
test_printf(" %s [%.*s]\n", label_1, UNPACK(url_1.host_text));
|
||||||
test_printf(" expected [%.*s]\n", UNPACK(expected.host_text));
|
test_printf(" %s [%.*s]\n", label_2, UNPACK(url_2.host_text));
|
||||||
ok = false;
|
ok = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsed.port != expected.port) {
|
if (url_1.port != url_2.port) {
|
||||||
if (ok) test_printf("\n");
|
if (ok) test_printf("\n");
|
||||||
test_printf(" port mismatch\n");
|
test_printf(" port mismatch\n");
|
||||||
test_printf(" parsed [%d]\n", parsed.port);
|
test_printf(" %s [%d]\n", label_1, url_1.port);
|
||||||
test_printf(" expected [%d]\n", expected.port);
|
test_printf(" %s [%d]\n", label_2, url_2.port);
|
||||||
ok = false;
|
ok = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!streq(parsed.path, expected.path)) {
|
if (!streq(url_1.path, url_2.path)) {
|
||||||
if (ok) test_printf("\n");
|
if (ok) test_printf("\n");
|
||||||
test_printf(" path mismatch\n");
|
test_printf(" path mismatch\n");
|
||||||
test_printf(" parsed [%.*s]\n", UNPACK(parsed.path));
|
test_printf(" %s [%.*s]\n", label_1, UNPACK(url_1.path));
|
||||||
test_printf(" expected [%.*s]\n", UNPACK(expected.path));
|
test_printf(" %s [%.*s]\n", label_2, UNPACK(url_2.path));
|
||||||
ok = false;
|
ok = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = url_percent_decode(parsed.query, tmp, sizeof(tmp));
|
if (!streq(url_1.query, url_2.query)) {
|
||||||
assert(ret > -1 && ret < (int) sizeof(tmp));
|
|
||||||
|
|
||||||
if (!streq((URL_String) { tmp, ret }, expected.query)) {
|
|
||||||
if (ok) test_printf("\n");
|
if (ok) test_printf("\n");
|
||||||
test_printf(" query mismatch\n");
|
test_printf(" query mismatch\n");
|
||||||
test_printf(" parsed [%.*s]\n", ret, tmp);
|
test_printf(" %s [%.*s]\n", label_1, UNPACK(url_1.query));
|
||||||
test_printf(" expected [%.*s]\n", UNPACK(expected.query));
|
test_printf(" %s [%.*s]\n", label_2, UNPACK(url_2.query));
|
||||||
ok = false;
|
ok = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = url_percent_decode(parsed.fragment, tmp, sizeof(tmp));
|
if (!streq(url_1.fragment, url_2.fragment)) {
|
||||||
assert(ret > -1 && ret < (int) sizeof(tmp));
|
|
||||||
|
|
||||||
if (!streq((URL_String) { tmp, ret }, expected.fragment)) {
|
|
||||||
if (ok) test_printf("\n");
|
if (ok) test_printf("\n");
|
||||||
test_printf(" fragment mismatch\n");
|
test_printf(" fragment mismatch\n");
|
||||||
test_printf(" parsed [%.*s]\n", ret, tmp);
|
test_printf(" %s [%.*s]\n", label_1, UNPACK(url_1.fragment));
|
||||||
test_printf(" expected [%.*s]\n", UNPACK(expected.fragment));
|
test_printf(" %s [%.*s]\n", label_2, UNPACK(url_2.fragment));
|
||||||
ok = false;
|
ok = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,7 +394,7 @@ static int run_test(cJSON *json)
|
|||||||
if (json_to_url(json, &expected) < 0)
|
if (json_to_url(json, &expected) < 0)
|
||||||
return -1;
|
return -1;
|
||||||
|
|
||||||
if (!compare_urls(parsed, expected)) {
|
if (!compare_urls(parsed, expected, "parsed", "expected")) {
|
||||||
test_printf(" input: ");
|
test_printf(" input: ");
|
||||||
test_print_str(src);
|
test_print_str(src);
|
||||||
test_printf("\n");
|
test_printf("\n");
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
# 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"
|
|
||||||
" "
|
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
#define UINT16_MAX (URL_u16) ((1 << 16)-1)
|
#define UINT16_MAX (URL_u16) ((1 << 16)-1)
|
||||||
|
|
||||||
#define NULL ((void*) 0)
|
#define NULL ((void*) 0)
|
||||||
#define S(X) ((URL_String) { (X), sizeof(X)-1 })
|
#define S(X) (URL_String) { (X), sizeof(X)-1 }
|
||||||
#define EMPTY (URL_String) { NULL, 0 }
|
#define EMPTY (URL_String) { NULL, 0 }
|
||||||
#define SLICE(src, off, end) (URL_String) { src + off, end - off }
|
#define SLICE(src, off, end) (URL_String) { src + off, end - off }
|
||||||
|
|
||||||
@@ -49,6 +49,14 @@ static void memcpy_(char *dst, char *src, int len)
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
static int strlen_(char *s)
|
||||||
|
{
|
||||||
|
int n = 0;
|
||||||
|
while (s[n] != '\0')
|
||||||
|
n++;
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
static URL_b32 streq(URL_String a, URL_String b)
|
static URL_b32 streq(URL_String a, URL_String b)
|
||||||
{
|
{
|
||||||
if (a.len != b.len)
|
if (a.len != b.len)
|
||||||
@@ -524,20 +532,9 @@ int url_parse(char *src, int len, int *pcur, URL *out, int flags)
|
|||||||
// First, try the IPv4 rule
|
// First, try the IPv4 rule
|
||||||
if (cur < len && is_digit(src[cur])) {
|
if (cur < len && is_digit(src[cur])) {
|
||||||
if (url_parse_ipv4(src, len, &cur, &out->host_ipv4) == 0) {
|
if (url_parse_ipv4(src, len, &cur, &out->host_ipv4) == 0) {
|
||||||
// If we managed to parse an IPv4 address but the
|
out->host_type = URL_HOST_IPV4;
|
||||||
// following character si a valid character for a
|
is_ipv4 = 1;
|
||||||
// registered name, then it wasn't an IPv4 after
|
|
||||||
// all. For instance this should not be parsed as
|
|
||||||
// an IPv4:
|
|
||||||
// 127.0.0.1.com
|
|
||||||
if (cur == len || !is_reg_name(src[cur])) {
|
|
||||||
out->host_type = URL_HOST_IPV4;
|
|
||||||
is_ipv4 = 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_ipv4)
|
|
||||||
cur = host_off;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_ipv4) {
|
if (!is_ipv4) {
|
||||||
@@ -795,6 +792,13 @@ static void append(Builder *b, URL_String s)
|
|||||||
b->len += s.len;
|
b->len += s.len;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void appendc(Builder *b, char c)
|
||||||
|
{
|
||||||
|
if (b->len < b->cap)
|
||||||
|
b->dst[b->len] = c;
|
||||||
|
b->len++;
|
||||||
|
}
|
||||||
|
|
||||||
static void append_port(Builder *b, URL_u16 port)
|
static void append_port(Builder *b, URL_u16 port)
|
||||||
{
|
{
|
||||||
char buf[sizeof("65536")-1];
|
char buf[sizeof("65536")-1];
|
||||||
@@ -1009,3 +1013,162 @@ int url_percent_decode(URL_String str, char *dst, int cap)
|
|||||||
}
|
}
|
||||||
return wr;
|
return wr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void append_percent_encoded(Builder *b, URL_String str, URL_b32 (*testfn)(char c))
|
||||||
|
{
|
||||||
|
for (int i = 0; i < str.len; i++) {
|
||||||
|
char c = str.ptr[i];
|
||||||
|
if (testfn(c))
|
||||||
|
appendc(b, str.ptr[i]);
|
||||||
|
else {
|
||||||
|
static const char table[] = "0123456789abcdef";
|
||||||
|
appendc(b, '%');
|
||||||
|
appendc(b, table[(URL_u8) c >> 4]);
|
||||||
|
appendc(b, table[(URL_u8) c & 0xF]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void url_builder_init(URL_Builder *ub)
|
||||||
|
{
|
||||||
|
ub->scheme = EMPTY;
|
||||||
|
ub->username = EMPTY;
|
||||||
|
ub->password = EMPTY;
|
||||||
|
ub->host_type = URL_HOST_EMPTY;
|
||||||
|
ub->no_port = 1;
|
||||||
|
ub->port = 0;
|
||||||
|
ub->path = EMPTY;
|
||||||
|
ub->query = EMPTY;
|
||||||
|
ub->fragment = EMPTY;
|
||||||
|
}
|
||||||
|
|
||||||
|
void url_builder_set_scheme(URL_Builder *ub, char *str, int len)
|
||||||
|
{
|
||||||
|
if (len < 0) len = str ? strlen_(str) : 0;
|
||||||
|
ub->scheme = (URL_String) { str, len };
|
||||||
|
}
|
||||||
|
|
||||||
|
void url_builder_set_username(URL_Builder *ub, char *str, int len)
|
||||||
|
{
|
||||||
|
if (len < 0) len = str ? strlen_(str) : 0;
|
||||||
|
ub->username = (URL_String) { str, len };
|
||||||
|
}
|
||||||
|
|
||||||
|
void url_builder_set_password(URL_Builder *ub, char *str, int len)
|
||||||
|
{
|
||||||
|
if (len < 0) len = str ? strlen_(str) : 0;
|
||||||
|
ub->password = (URL_String) { str, len };
|
||||||
|
}
|
||||||
|
|
||||||
|
void url_builder_set_host_name(URL_Builder *ub, char *str, int len)
|
||||||
|
{
|
||||||
|
if (len < 0) len = str ? strlen_(str) : 0;
|
||||||
|
ub->host_type = URL_HOST_NAME;
|
||||||
|
ub->host_name = (URL_String) { str, len };
|
||||||
|
}
|
||||||
|
|
||||||
|
void url_builder_set_host_ipv4(URL_Builder *ub, URL_IPv4 ipv4)
|
||||||
|
{
|
||||||
|
ub->host_type = URL_HOST_IPV4;
|
||||||
|
ub->host_ipv4 = ipv4;
|
||||||
|
}
|
||||||
|
|
||||||
|
void url_builder_set_host_ipv6(URL_Builder *ub, URL_IPv6 ipv6)
|
||||||
|
{
|
||||||
|
ub->host_type = URL_HOST_IPV6;
|
||||||
|
ub->host_ipv6 = ipv6;
|
||||||
|
}
|
||||||
|
|
||||||
|
void url_builder_set_port(URL_Builder *ub, URL_u16 port)
|
||||||
|
{
|
||||||
|
ub->no_port = 0;
|
||||||
|
ub->port = port;
|
||||||
|
}
|
||||||
|
|
||||||
|
void url_builder_set_path(URL_Builder *ub, char *str, int len)
|
||||||
|
{
|
||||||
|
if (len < 0) len = str ? strlen_(str) : 0;
|
||||||
|
ub->path = (URL_String) { str, len };
|
||||||
|
}
|
||||||
|
|
||||||
|
void url_builder_set_query(URL_Builder *ub, char *str, int len)
|
||||||
|
{
|
||||||
|
if (len < 0) len = str ? strlen_(str) : 0;
|
||||||
|
ub->query = (URL_String) { str, len };
|
||||||
|
}
|
||||||
|
|
||||||
|
void url_builder_set_fragment(URL_Builder *ub, char *str, int len)
|
||||||
|
{
|
||||||
|
if (len < 0) len = str ? strlen_(str) : 0;
|
||||||
|
ub->fragment = (URL_String) { str, len };
|
||||||
|
}
|
||||||
|
|
||||||
|
int url_builder_finalize(URL_Builder *ub, char *dst, int cap)
|
||||||
|
{
|
||||||
|
Builder b = { dst, cap, 0 };
|
||||||
|
|
||||||
|
// Validate and append the schema
|
||||||
|
{
|
||||||
|
if (ub->scheme.len == 0 || !is_scheme_first(ub->scheme.ptr[0]))
|
||||||
|
return -1;
|
||||||
|
for (int i = 1; i < ub->scheme.len; i++)
|
||||||
|
if (!is_scheme(ub->scheme.ptr[i]))
|
||||||
|
return -1;
|
||||||
|
append(&b, ub->scheme);
|
||||||
|
}
|
||||||
|
|
||||||
|
URL_b32 authority = 0;
|
||||||
|
if (ub->username.len > 0 || ub->password.len > 0 || ub->host_type != URL_HOST_EMPTY || ub->no_port == 0) {
|
||||||
|
authority = 1;
|
||||||
|
appendc(&b, '/');
|
||||||
|
appendc(&b, '/');
|
||||||
|
if (ub->username.len > 0 || ub->password.len > 0) {
|
||||||
|
append_percent_encoded(&b, ub->username, is_userinfo);
|
||||||
|
appendc(&b, ':');
|
||||||
|
append_percent_encoded(&b, ub->password, is_userinfo);
|
||||||
|
appendc(&b, '@');
|
||||||
|
}
|
||||||
|
switch (ub->host_type) {
|
||||||
|
case URL_HOST_EMPTY:
|
||||||
|
// Do nothing
|
||||||
|
break;
|
||||||
|
case URL_HOST_NAME:
|
||||||
|
append_percent_encoded(&b, ub->host_name, is_reg_name);
|
||||||
|
break;
|
||||||
|
case URL_HOST_IPV4:
|
||||||
|
ASSERT(0); // TODO
|
||||||
|
break;
|
||||||
|
case URL_HOST_IPV6:
|
||||||
|
appendc(&b, '[');
|
||||||
|
ASSERT(0);
|
||||||
|
appendc(&b, ']');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (ub->no_port == 0) {
|
||||||
|
appendc(&b, ':');
|
||||||
|
append_port(&b, ub->port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ub->path.len > 0) {
|
||||||
|
|
||||||
|
// If an authority is present, the path can't be relative
|
||||||
|
if (authority && ub->path.ptr[0] != '/')
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
PathComps comps = {0};
|
||||||
|
if (resolve_dots_and_append_comps(&comps, ub->path) < 0)
|
||||||
|
return -1;
|
||||||
|
for (int i = 0; i < comps.count; i++) {
|
||||||
|
if (i > 0 || comps.first_slash)
|
||||||
|
append(&b, S("/"));
|
||||||
|
append_percent_encoded(&b, comps.stack[i], is_pchar);
|
||||||
|
}
|
||||||
|
if (comps.trailing_slash)
|
||||||
|
append(&b, S("/"));
|
||||||
|
}
|
||||||
|
|
||||||
|
append_query(&b, ub->query);
|
||||||
|
append_fragment(&b, ub->fragment);
|
||||||
|
return b.len;
|
||||||
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ typedef enum {
|
|||||||
URL_HOST_NAME,
|
URL_HOST_NAME,
|
||||||
} URL_HostType;
|
} URL_HostType;
|
||||||
|
|
||||||
|
// This struct's fields are strictly read-only.
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
|
||||||
// Flags used to parse this URL
|
// Flags used to parse this URL
|
||||||
@@ -174,4 +175,39 @@ int url_percent_decode(URL_String str, char *dst, int cap);
|
|||||||
// though it's just a best-effor normalization for now.
|
// though it's just a best-effor normalization for now.
|
||||||
int url_serialize(URL url, URL *base, char *dst, int cap);
|
int url_serialize(URL url, URL *base, char *dst, int cap);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
|
||||||
|
URL_String scheme;
|
||||||
|
|
||||||
|
URL_String username;
|
||||||
|
URL_String password;
|
||||||
|
|
||||||
|
URL_HostType host_type;
|
||||||
|
union {
|
||||||
|
URL_String host_name;
|
||||||
|
URL_IPv4 host_ipv4;
|
||||||
|
URL_IPv6 host_ipv6;
|
||||||
|
};
|
||||||
|
|
||||||
|
URL_b32 no_port;
|
||||||
|
URL_u16 port;
|
||||||
|
|
||||||
|
URL_String path;
|
||||||
|
URL_String query;
|
||||||
|
URL_String fragment;
|
||||||
|
} URL_Builder;
|
||||||
|
|
||||||
|
void url_builder_init(URL_Builder *ub);
|
||||||
|
void url_builder_set_scheme(URL_Builder *ub, char *str, int len);
|
||||||
|
void url_builder_set_username(URL_Builder *ub, char *str, int len);
|
||||||
|
void url_builder_set_password(URL_Builder *ub, char *str, int len);
|
||||||
|
void url_builder_set_host_name(URL_Builder *ub, char *str, int len);
|
||||||
|
void url_builder_set_host_ipv4(URL_Builder *ub, URL_IPv4 ipv4);
|
||||||
|
void url_builder_set_host_ipv6(URL_Builder *ub, URL_IPv6 ipv6);
|
||||||
|
void url_builder_set_port(URL_Builder *ub, URL_u16 port);
|
||||||
|
void url_builder_set_path(URL_Builder *ub, char *str, int len);
|
||||||
|
void url_builder_set_query(URL_Builder *ub, char *str, int len);
|
||||||
|
void url_builder_set_fragment(URL_Builder *ub, char *str, int len);
|
||||||
|
int url_builder_finalize(URL_Builder *ub, char *dst, int cap);
|
||||||
|
|
||||||
#endif // URL_INCLUDED
|
#endif // URL_INCLUDED
|
||||||
|
|||||||
Reference in New Issue
Block a user