run-fuzzers.php•8.2 kB
#!/usr/bin/env php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
// Configure fuzzing parameters
$maxIterations = 10000;
$timeout = (int) ($_ENV['FUZZING_DURATION'] ?? 300);
$corpusDir = __DIR__ . '/corpus';
$crashDir = __DIR__ . '/crashes';
// Ensure directories exist
if (! is_dir($corpusDir)) {
mkdir($corpusDir, 0o755, true);
}
if (! is_dir($crashDir)) {
mkdir($crashDir, 0o755, true);
}
// Get all fuzzing targets
$targetFiles = glob(__DIR__ . '/Targets/*Target.php');
$exitCode = 0;
$totalCrashes = 0;
echo "Starting fuzzing session for {$timeout} seconds...\n";
echo 'Found ' . count($targetFiles) . " fuzzing targets\n\n";
// Simple fuzzing implementation since php-fuzzer is meant to be used as CLI tool
function generateRandomInput(int $maxLen = 1000): string
{
$len = random_int(0, $maxLen);
$input = '';
// Mix of different input types
$strategy = random_int(0, 4);
switch ($strategy) {
case 0: // Random bytes
for ($i = 0; $i < $len; $i++) {
$input .= chr(random_int(0, 255));
}
break;
case 1: // Printable ASCII
for ($i = 0; $i < $len; $i++) {
$input .= chr(random_int(32, 126));
}
break;
case 2: // Alphanumeric with special chars
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;:,.<>?';
for ($i = 0; $i < $len; $i++) {
$input .= $chars[random_int(0, strlen($chars) - 1)];
}
break;
case 3: // Unicode mix
for ($i = 0; $i < $len / 3; $i++) {
$input .= mb_chr(random_int(0x80, 0x10FFFF));
}
break;
case 4: // Structured data patterns
$patterns = [
'user:' . str_repeat('a', random_int(0, 100)) . '#reader@doc:test',
'model\n schema 1.1\ntype ' . str_repeat('x', random_int(0, 50)),
str_repeat('nested(', random_int(0, 20)) . 'value' . str_repeat(')', random_int(0, 20)),
'{"key":"' . str_repeat('v', random_int(0, 100)) . '"}',
];
$input = $patterns[random_int(0, count($patterns) - 1)];
break;
}
return $input;
}
function mutateInput(string $input): string
{
if (empty($input)) {
return generateRandomInput();
}
$strategy = random_int(0, 5);
switch ($strategy) {
case 0: // Bit flip
$pos = random_int(0, strlen($input) - 1);
$input[$pos] = chr(ord($input[$pos]) ^ (1 << random_int(0, 7)));
break;
case 1: // Insert random byte
$pos = random_int(0, strlen($input));
$input = substr($input, 0, $pos) . chr(random_int(0, 255)) . substr($input, $pos);
break;
case 2: // Delete byte
if (1 < strlen($input)) {
$pos = random_int(0, strlen($input) - 1);
$input = substr($input, 0, $pos) . substr($input, $pos + 1);
}
break;
case 3: // Duplicate section
$len = strlen($input);
if (0 < $len) {
$start = random_int(0, $len - 1);
$end = random_int($start, $len - 1);
$section = substr($input, $start, $end - $start + 1);
$input .= $section;
}
break;
case 4: // Replace with interesting values
$interesting = ["\x00", "\xff", "\n", "\r\n", '\\', "'", '"', "\t", str_repeat('A', 1000)];
$input = $interesting[random_int(0, count($interesting) - 1)];
break;
case 5: // Shuffle parts
$parts = str_split($input, max(1, (int) (strlen($input) / 4)));
shuffle($parts);
$input = implode('', $parts);
break;
}
return $input;
}
foreach ($targetFiles as $targetFile) {
$targetName = basename($targetFile, '.php');
echo "Running fuzzer: {$targetName}\n";
// Build the target class name
$targetClass = "OpenFGA\\MCP\\Tests\\Fuzzing\\Targets\\{$targetName}";
// Check if class already exists (autoloader should handle this)
if (! class_exists($targetClass)) {
echo " ERROR: Target class {$targetClass} not found\n";
continue;
}
$target = new $targetClass;
// Create corpus directory for this target
$targetCorpusDir = "{$corpusDir}/{$targetName}";
if (! is_dir($targetCorpusDir)) {
mkdir($targetCorpusDir, 0o755, true);
}
// Load initial corpus
$corpus = [];
if (method_exists($target, 'getInitialCorpus')) {
$corpus = $target->getInitialCorpus();
}
// Load saved corpus
$corpusFiles = glob("{$targetCorpusDir}/*.txt");
foreach ($corpusFiles as $file) {
$corpus[] = file_get_contents($file);
}
// If no corpus, generate some random inputs
if (empty($corpus)) {
for ($i = 0; 10 > $i; $i++) {
$corpus[] = generateRandomInput();
}
}
// Run fuzzing for a portion of the total time
$targetTimeout = (int) ($timeout / count($targetFiles));
$startTime = time();
$endTime = $startTime + $targetTimeout;
$iterations = 0;
$crashes = 0;
echo " Fuzzing for {$targetTimeout} seconds...\n";
echo ' Initial corpus size: ' . count($corpus) . "\n";
while (time() < $endTime && $iterations < $maxIterations) {
// Pick an input from corpus or generate new one
if (! empty($corpus) && 0 === random_int(0, 1)) {
$input = $corpus[array_rand($corpus)];
$input = mutateInput($input);
} else {
$input = generateRandomInput();
}
// Run the fuzzing target
try {
$target->fuzz($input);
// If it didn't crash, maybe add to corpus
if (0 === random_int(0, 100)) {
$corpus[] = $input;
// Save interesting inputs
if (0 === count($corpus) % 10) {
$hash = substr(md5($input), 0, 8);
$corpusFile = "{$targetCorpusDir}/corpus_{$hash}.txt";
file_put_contents($corpusFile, $input);
}
}
} catch (Throwable $e) {
// Check if this is an expected error
if (method_exists($target, 'isExpectedError') && $target->isExpectedError($e)) {
// Expected error, continue
} else {
// Unexpected crash - save it
$crashes++;
$crashHash = substr(md5($input . $e->getMessage()), 0, 8);
$crashFile = "{$crashDir}/{$targetName}_{$crashHash}.crash";
if (! file_exists($crashFile)) {
file_put_contents($crashFile, json_encode([
'target' => $targetName,
'error' => $e->getMessage(),
'type' => $e::class,
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString(),
'input' => $input,
'input_hex' => bin2hex($input),
'input_length' => strlen($input),
], JSON_PRETTY_PRINT));
echo ' 💥 Found crash: ' . substr($e->getMessage(), 0, 50) . "...\n";
}
}
}
$iterations++;
}
$elapsed = time() - $startTime;
echo " ✓ Completed {$iterations} iterations in {$elapsed}s\n";
if (0 < $crashes) {
echo " ⚠️ Found {$crashes} crashes\n";
$totalCrashes += $crashes;
$exitCode = 1;
}
echo "\n";
}
echo "Fuzzing session completed\n";
// Check for any crashes
$allCrashes = glob("{$crashDir}/*.crash");
if (! empty($allCrashes)) {
echo "\n⚠️ Total crashes found: " . count($allCrashes) . "\n";
foreach ($allCrashes as $crash) {
echo ' - ' . basename($crash) . "\n";
}
exit(1);
}
exit($exitCode);