Skip to main content
Glama

OpenFGA MCP

APIResponseTarget.php11.2 kB
<?php declare(strict_types=1); namespace OpenFGA\MCP\Tests\Fuzzing\Targets; use Exception; use Throwable; use function count; use function is_array; use function is_int; use function is_string; use function strlen; /** * Fuzzing target for API response parsing and handling. */ final class APIResponseTarget { private const MAX_ARRAY_DEPTH = 10; private const MAX_RESPONSE_SIZE = 10 * 1024 * 1024; // 10MB private const MAX_TUPLE_COUNT = 10000; public function fuzz(string $input): void { // Test different response formats $this->testJSONParsing($input); $this->testTupleResponseParsing($input); $this->testStoreListParsing($input); $this->testErrorResponseParsing($input); } public function getInitialCorpus(): array { return [ // Valid responses '{"tuples":[{"key":{"user":"user:1","relation":"viewer","object":"doc:1"}}]}', '{"stores":[{"id":"store1","name":"Test Store","created_at":"2023-01-01T00:00:00Z"}]}', '{"tuples":[],"continuation_token":"eyJwayI6IkxBVEVTVF9WRVJTSU9OIn0"}', '{"error":{"code":"invalid_request","message":"Invalid tuple format"}}', // Edge cases '{}', '[]', 'null', 'true', '"string"', '12345', // Malformed JSON '{tuples:[]}', '{"tuples":[{"key":}]}', '{"unclosed":', // Deep nesting '{"a":{"b":{"c":{"d":{"e":{"f":{"g":{"h":{"i":{"j":{}}}}}}}}}}}', // Large responses '{"tuples":[' . str_repeat('{"key":{"user":"u","relation":"r","object":"o"}},', 100) . ']}', // Injection attempts '{"tuples":[{"key":{"user":"<script>alert(1)</script>","relation":"viewer","object":"doc:1"}}]}', '{"continuation_token":"../../etc/passwd"}', '{"stores":[{"id":"store1","name":"Test\u0000Null"}]}', // Unicode edge cases '{"message":"Test\u200e\u200f\u202a\u202b\u202c\u202d\u202e"}', '{"user":"user:\ud800\udc00"}', // Surrogate pair // Type confusion '{"tuples":{"0":{"key":{"user":"user:1"}}}}', '{"stores":"not-an-array"}', '{"continuation_token":12345}', ]; } public function isExpectedError(Throwable $e): bool { $expectedMessages = [ 'Response too large', 'Null byte in JSON', 'Array nesting too deep', 'Object key too long', 'String value too long', 'Too many tuples in response', 'Invalid timestamp format', 'Invalid continuation token type', 'Continuation token too long', 'Tuple key must be object', 'User must be string', 'User identifier too long in response', 'Control characters in user field', 'Relation must be string', 'Relation too long in response', 'Invalid relation format in response', 'Object must be string', 'Object identifier too long in response', 'Store ID must be string', 'Store name must be string', 'Store name too long', 'Invalid created_at format', 'Invalid updated_at format', 'Error code must be string or integer', 'Error message must be string', 'Error message too long', 'Stack trace exposed in error response', ]; foreach ($expectedMessages as $expected) { if (false !== stripos($e->getMessage(), $expected)) { return true; } } return false; } private function testErrorResponseParsing(string $input): void { $decoded = @json_decode($input, true); if (! is_array($decoded)) { return; } // Check error response structure if (isset($decoded['error']) || isset($decoded['code']) || isset($decoded['message'])) { // Validate error code if (isset($decoded['code'])) { if (! is_string($decoded['code']) && ! is_int($decoded['code'])) { throw new Exception('Error code must be string or integer'); } } // Validate error message if (isset($decoded['message'])) { if (! is_string($decoded['message'])) { throw new Exception('Error message must be string'); } if (10000 < strlen($decoded['message'])) { throw new Exception('Error message too long'); } } // Check for stack traces (should not be exposed) if (isset($decoded['stack']) || isset($decoded['trace'])) { throw new Exception('Stack trace exposed in error response'); } } } private function testJSONParsing(string $input): void { if (self::MAX_RESPONSE_SIZE < strlen($input)) { throw new Exception('Response too large'); } // Try to parse as JSON if (empty(trim($input))) { return; } // Check for null bytes if (str_contains($input, "\0")) { throw new Exception('Null byte in JSON'); } $decoded = json_decode($input, true, self::MAX_ARRAY_DEPTH); if (JSON_ERROR_NONE !== json_last_error()) { // This is expected for many fuzz inputs return; } // Validate decoded structure if (is_array($decoded)) { $this->validateArrayStructure($decoded, 0); } } private function testStoreListParsing(string $input): void { $decoded = @json_decode($input, true); if (! is_array($decoded)) { return; } // Check stores array if (isset($decoded['stores']) && is_array($decoded['stores'])) { foreach ($decoded['stores'] as $store) { if (! is_array($store)) { continue; } // Validate store ID if (isset($store['id']) && ! is_string($store['id'])) { throw new Exception('Store ID must be string'); } // Validate store name if (isset($store['name'])) { if (! is_string($store['name'])) { throw new Exception('Store name must be string'); } if (256 < strlen($store['name'])) { throw new Exception('Store name too long'); } } // Check timestamps foreach (['created_at', 'updated_at'] as $field) { if (isset($store[$field]) && ! is_string($store[$field])) { throw new Exception("Invalid {$field} format"); } } } } } private function testTupleResponseParsing(string $input): void { // Simulate parsing tuple response $decoded = @json_decode($input, true); if (! is_array($decoded)) { return; } // Check for tuples array if (isset($decoded['tuples']) && is_array($decoded['tuples'])) { if (self::MAX_TUPLE_COUNT < count($decoded['tuples'])) { throw new Exception('Too many tuples in response'); } foreach ($decoded['tuples'] as $tuple) { if (! is_array($tuple)) { continue; } // Validate tuple structure if (isset($tuple['key'])) { $this->validateTupleKey($tuple['key']); } // Check for timestamp if (isset($tuple['timestamp']) && ! is_string($tuple['timestamp'])) { throw new Exception('Invalid timestamp format'); } } } // Check continuation token if (isset($decoded['continuation_token'])) { if (! is_string($decoded['continuation_token'])) { throw new Exception('Invalid continuation token type'); } if (1024 < strlen($decoded['continuation_token'])) { throw new Exception('Continuation token too long'); } // Check for suspicious patterns in token if (preg_match('/[<>\'"]/', $decoded['continuation_token'])) { // Potential injection } } } private function validateArrayStructure(array $data, int $depth): void { if (self::MAX_ARRAY_DEPTH < $depth) { throw new Exception('Array nesting too deep'); } foreach ($data as $key => $value) { // Validate key length if (is_string($key) && 256 < strlen($key)) { throw new Exception('Object key too long'); } // Recursively validate nested structures if (is_array($value)) { $this->validateArrayStructure($value, $depth + 1); } elseif (is_string($value)) { // Check string length if (strlen($value) > 1024 * 1024) { throw new Exception('String value too long'); } } } } private function validateTupleKey($key): void { if (! is_array($key)) { throw new Exception('Tuple key must be object'); } // Validate user field if (isset($key['user'])) { if (! is_string($key['user'])) { throw new Exception('User must be string'); } if (512 < strlen($key['user'])) { throw new Exception('User identifier too long in response'); } // Check for control characters if (preg_match('/[\x00-\x1F\x7F]/', $key['user'])) { throw new Exception('Control characters in user field'); } } // Validate relation field if (isset($key['relation'])) { if (! is_string($key['relation'])) { throw new Exception('Relation must be string'); } if (50 < strlen($key['relation'])) { throw new Exception('Relation too long in response'); } if (! preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $key['relation'])) { throw new Exception('Invalid relation format in response'); } } // Validate object field if (isset($key['object'])) { if (! is_string($key['object'])) { throw new Exception('Object must be string'); } if (512 < strlen($key['object'])) { throw new Exception('Object identifier too long in response'); } } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/evansims/openfga-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server