# PHP Fuzzer PHP Fuzzer is a feedback-driven fuzzing library for PHP that helps discover bugs in libraries by testing them with automatically generated inputs. It uses edge coverage instrumentation to track which code paths are executed, guiding the mutation engine to explore new code branches and find edge cases that cause crashes, errors, or timeouts. The fuzzer is particularly effective for testing parsing libraries where malformed inputs might cause unexpected behavior. It automatically instruments PHP code to collect coverage feedback, mutates inputs using strategies based on libFuzzer, and maintains a corpus of interesting test cases. Bugs are detected through uncaught `Error` exceptions, notices/warnings, and execution timeouts. ## Installation ### Install via Phar (Recommended) Download the phar package from the releases page to avoid dependency conflicts. ```bash # Download from GitHub releases wget https://github.com/nikic/PHP-Fuzzer/releases/download/v0.2.0/php-fuzzer.phar chmod +x php-fuzzer.phar # Verify installation ./php-fuzzer.phar --help ``` ### Install via Composer ```bash composer global require nikic/php-fuzzer ``` ## Config::setTarget() Sets the fuzzing target function that will be called with generated inputs. The target accepts a single string input and processes it through the library being tested. Normal `Exception` exceptions are ignored, but `Error` exceptions indicate a discovered bug. ```php setTarget(function(string $input) { // Any Error thrown here is considered a bug // Exception is allowed (malformed input is expected) json_decode($input, true, 512, JSON_THROW_ON_ERROR); }); ``` ## Config::setMaxLen() Sets the maximum length of generated inputs in bytes. Limiting input length improves performance since many bugs can be found with shorter inputs. The fuzzer also implements automatic length control, starting with short inputs and gradually increasing length. ```php setTarget(function(string $input) use ($parser) { $parser->parse($input); }); // Limit inputs to 1KB - good for most parsing targets $config->setMaxLen(1024); ``` ## Config::addDictionary() Adds a dictionary file containing useful fragments for the fuzzer. Dictionaries are essential when fuzzing parsers that handle language keywords, as the fuzzer may struggle to discover these through random mutation alone. ```php setTarget(function(string $input) use ($parser) { $parser->parseSourceFile($input); }); $config->setMaxLen(1024); // Add PHP keywords dictionary to help discover language constructs $config->addDictionary(__DIR__ . '/php.dict'); ``` Dictionary file format (php.dict): ``` "" "function" "class" "interface" "if" "else" "while" "for" "foreach" "return" "yield" "try" "catch" "throw" "namespace" "use" ``` ## Config::setAllowedExceptions() Configures which exception types are considered normal and should not be reported as bugs. By default, only `Exception` is allowed. Use this to whitelist parser-specific exceptions that are expected for invalid input. ```php setTarget(function(string $input) use ($parser, $prettyPrinter) { $stmts = $parser->parse($input); $prettyPrinter->prettyPrintFile($stmts); }); $config->setMaxLen(1024); $config->addDictionary(__DIR__ . '/php.dict'); // Allow PhpParser\Error as normal behavior for invalid PHP code $config->setAllowedExceptions([PhpParser\Error::class]); ``` ## CLI: fuzz Command The main fuzzing command that runs the fuzzer against a target. It continuously generates mutated inputs, executes them through the target, and builds a corpus of interesting test cases. Crashes are saved to `crash-HASH.txt` files. ```bash # Run fuzzer without initial corpus (creates temporary directory) php-fuzzer fuzz target.php # Run with existing corpus directory (one input per file) php-fuzzer fuzz target.php corpus/ # Run with options php-fuzzer --max-runs=100000 --timeout=5 fuzz target.php corpus/ # Run with dictionary php-fuzzer --dict=php.dict fuzz target.php corpus/ # Example output during fuzzing: # NEW run: 1234 (500/s), ft: 456 (10/s), corp: 89 (12kb), len: 128/1024, t: 2s, mem: 4mb # REDUCE run: 2000 (480/s), ft: 460 (9/s), corp: 90 (11kb), len: 64/1024, t: 4s, mem: 4mb ``` ## CLI: minimize-crash Command Reduces a crashing input to its minimal form while preserving the crash. This helps identify the exact cause of the bug by removing unnecessary bytes from the input. ```bash # Minimize a crash file php-fuzzer minimize-crash target.php crash-abc123.txt # Output produces successively smaller files: # CRASH with length 512 in minimized-def456.txt! # CRASH with length 256 in minimized-ghi789.txt! # CRASH with length 64 in minimized-jkl012.txt! # Limit minimization runs php-fuzzer --max-runs=50000 minimize-crash target.php crash-abc123.txt ``` ## CLI: run-single Command Executes a single input through the target and displays any crash information. Useful for debugging and verifying that a minimized crash still reproduces. ```bash # Run a single test case through the target php-fuzzer run-single target.php minimized-abc123.txt # Example output for crashing input: # CRASH in minimized-abc123.txt! # Error: Call to undefined method on null in /path/to/library.php:123 # Stack trace: # #0 /path/to/library.php(100): Parser->process() # #1 target.php(15): Parser->parse() ``` ## CLI: report-coverage Command Generates an HTML coverage report showing which code blocks are hit when executing corpus inputs. This helps identify under-tested areas of the target library. ```bash # Generate coverage report php-fuzzer report-coverage target.php corpus/ coverage_report/ # View the report open coverage_report/index.html ``` ## Complete Target Example: CSS Parser A complete fuzzing target for testing a CSS parsing library with all configuration options. ```php setTarget(function(string $input) { $parser = new Sabberworm\CSS\Parser($input); $parser->parse(); }); $config->setMaxLen(2048); ``` ## Complete Target Example: Differential Testing Compare two implementations to find cases where they produce different results. This technique catches semantic bugs that don't cause exceptions. ```php setTarget(function(string $input) use ($parser1, $parser2) { try { $result1 = $parser1->parse($input); } catch (Exception $e) { $result1 = null; } try { $result2 = $parser2->parse($input); } catch (Exception $e) { $result2 = null; } // Throw Error if results differ - this will be caught as a bug if ($result1 != $result2) { throw new Error(sprintf( "Results do not match!\nParser1: %s\nParser2: %s", var_export($result1, true), var_export($result2, true) )); } }); $config->setMaxLen(512); ``` ## Complete Workflow Example A full workflow demonstrating corpus management, fuzzing, crash handling, and coverage analysis. ```bash # 1. Create initial corpus from existing test cases mkdir -p corpus cp tests/fixtures/*.txt corpus/ # 2. Start fuzzing session php-fuzzer fuzz target.php corpus/ # Press Ctrl+C to stop # 3. If crashes are found, minimize them php-fuzzer minimize-crash target.php crash-abc123.txt # 4. Debug the minimized crash php-fuzzer run-single target.php minimized-def456.txt # 5. Generate coverage report to find gaps php-fuzzer report-coverage target.php corpus/ coverage/ # 6. Resume fuzzing later (corpus state is preserved) php-fuzzer fuzz target.php corpus/ ``` ## Summary PHP Fuzzer is ideal for testing PHP parsing libraries, serialization code, validators, and any functionality that processes untrusted string input. Common use cases include fuzzing JSON/XML/YAML parsers, SQL query builders, template engines, protocol implementations, and file format readers. The tool has discovered bugs in popular libraries like tolerant-php-parser, PHP-CSS-Parser, league/uri, amphp/http-client, and phpmyadmin/sql-parser. Integration into development workflows typically involves creating a target file that wraps the library under test, seeding a corpus with representative inputs from unit tests, and running fuzzing sessions during CI or dedicated security testing. The coverage instrumentation automatically tracks execution paths, allowing the mutation engine to discover complex edge cases without manual test case creation. Crashes are saved for reproducibility, and the minimize-crash command helps isolate the root cause for faster debugging.