skills/dykyi-roman/awesome-claude-code/acc-check-deserialization

acc-check-deserialization

SKILL.md

Insecure Deserialization Security Check

Analyze PHP code for insecure deserialization vulnerabilities (OWASP A08:2021).

Detection Patterns

1. Unserialize with User Input

// CRITICAL: Direct user input
$data = unserialize($_GET['data']);
$object = unserialize($_POST['payload']);
$config = unserialize($_COOKIE['session']);

// CRITICAL: Request object
$data = unserialize($request->input('data'));

// CRITICAL: From file upload
$content = file_get_contents($_FILES['import']['tmp_name']);
$data = unserialize($content);

2. Missing allowed_classes Option

// CRITICAL: No allowed_classes restriction (PHP 7+)
$data = unserialize($serialized);
// Can instantiate ANY class

// CRITICAL: allowed_classes = true (default behavior)
$data = unserialize($serialized, ['allowed_classes' => true]);

// SECURE: No objects allowed
$data = unserialize($serialized, ['allowed_classes' => false]);

// SECURE: Whitelist specific classes
$data = unserialize($serialized, [
    'allowed_classes' => [User::class, Config::class],
]);

3. Gadget Chain Triggers

// VULNERABLE: Classes with dangerous magic methods
class FileHandler
{
    private string $path;

    // Called on unserialize - could delete arbitrary files
    public function __wakeup(): void
    {
        unlink($this->path);
    }
}

class Logger
{
    private string $logFile;
    private string $data;

    // Called when object destroyed - could write arbitrary files
    public function __destruct()
    {
        file_put_contents($this->logFile, $this->data);
    }
}

class DataLoader
{
    private string $source;

    // Called on property access - SSRF/file read
    public function __get($name)
    {
        return file_get_contents($this->source);
    }
}

class CommandRunner
{
    private string $command;

    // Called when object used as string - RCE
    public function __toString(): string
    {
        return shell_exec($this->command);
    }
}

4. Database-Stored Serialized Data

// CRITICAL: Trusting database content
$row = $db->query("SELECT settings FROM users WHERE id = ?", [$id]);
$settings = unserialize($row['settings']);
// If attacker can modify DB (SQL injection), they control serialized data

// CRITICAL: Session storage
$sessionData = unserialize($redis->get('session:' . $sessionId));

5. Phar Deserialization

// CRITICAL: Phar metadata triggers unserialize
$phar = new Phar($userUploadedFile);
// Reading phar file deserializes metadata

// CRITICAL: File operations on phar://
file_exists('phar://' . $uploadedFile);
file_get_contents('phar://' . $userFile);
is_dir('phar://' . $path);
fopen('phar://' . $filename, 'r');

// CRITICAL: include/require phar
include 'phar://' . $plugin . '/bootstrap.php';

6. Base64-Encoded Serialized Data

// CRITICAL: Common pattern to hide serialized data
$payload = base64_decode($_GET['token']);
$data = unserialize($payload);

// CRITICAL: In cookies
$sessionData = unserialize(base64_decode($_COOKIE['session']));

// CRITICAL: In headers
$auth = unserialize(base64_decode($request->header('X-Auth-Data')));

7. JSON with Object Mapping

// POTENTIALLY VULNERABLE: JSON to object without validation
$json = file_get_contents('php://input');
$data = json_decode($json);

// CRITICAL: Mapping to class with dangerous methods
$object = (new UserDTO())->fromArray(json_decode($json, true));
// If class has __set or __call that executes code

// CRITICAL: Doctrine/JMS Serializer without type whitelist
$user = $this->serializer->deserialize($json, 'object', 'json');

8. Cache Deserialization

// CRITICAL: Cache poisoning leads to object injection
$cacheKey = 'user_' . $userId;
$user = unserialize($cache->get($cacheKey));
// If cache can be poisoned, attacker controls object

// CRITICAL: File-based cache
$cached = file_get_contents("/tmp/cache/{$key}.cache");
$data = unserialize($cached);

9. Framework-Specific Patterns

// CRITICAL: Laravel signed URLs without validation
$data = unserialize($request->input('signed_data'));

// CRITICAL: Symfony serialized tokens
$token = unserialize($session->get('_security_main'));

// CRITICAL: Custom session handlers
class MySessionHandler implements SessionHandlerInterface
{
    public function read($id): string|false
    {
        $data = $this->storage->get($id);
        return unserialize($data); // Dangerous
    }
}

10. RPC/IPC Serialization

// CRITICAL: Inter-process communication
$message = unserialize(file_get_contents('php://stdin'));

// CRITICAL: Queue messages
$job = unserialize($queue->pop());

// CRITICAL: Socket data
$data = unserialize(socket_read($socket, 1024));

Grep Patterns

# unserialize calls
Grep: "unserialize\s*\(" --glob "**/*.php"

# unserialize with user input
Grep: "unserialize\s*\([^)]*(\\\$_GET|\\\$_POST|\\\$_COOKIE|\\\$_REQUEST)" --glob "**/*.php"

# phar:// usage
Grep: "phar://" --glob "**/*.php"

# Magic methods that could be exploited
Grep: "__wakeup|__destruct|__toString|__call\s*\(" --glob "**/*.php"

# Missing allowed_classes
Grep: "unserialize\s*\([^)]+\)\s*;" --glob "**/*.php"

Secure Patterns

Use JSON Instead

// SECURE: JSON for data interchange
$data = json_decode($input, true, 512, JSON_THROW_ON_ERROR);

// SECURE: Validate JSON structure
$data = json_decode($input, true);
if (!isset($data['expected_field'])) {
    throw new InvalidInputException();
}

Restrict allowed_classes

// SECURE: Only allow specific classes
$allowed = [
    UserDTO::class,
    ConfigDTO::class,
];

$data = unserialize($serialized, ['allowed_classes' => $allowed]);

// SECURE: No objects at all (for arrays/primitives)
$data = unserialize($serialized, ['allowed_classes' => false]);

Signature Verification

// SECURE: Sign serialized data
final class SecureSerializer
{
    public function __construct(
        private readonly string $secretKey,
    ) {}

    public function serialize(mixed $data): string
    {
        $serialized = serialize($data);
        $signature = hash_hmac('sha256', $serialized, $this->secretKey);
        return base64_encode($signature . $serialized);
    }

    public function unserialize(string $input, array $allowedClasses = []): mixed
    {
        $decoded = base64_decode($input, true);
        if ($decoded === false || strlen($decoded) < 64) {
            throw new SecurityException('Invalid data format');
        }

        $signature = substr($decoded, 0, 64);
        $serialized = substr($decoded, 64);

        $expected = hash_hmac('sha256', $serialized, $this->secretKey);
        if (!hash_equals($expected, $signature)) {
            throw new SecurityException('Invalid signature');
        }

        return unserialize($serialized, ['allowed_classes' => $allowedClasses]);
    }
}

Use Typed DTOs

// SECURE: Manual mapping to DTO
final readonly class CreateUserRequest
{
    public function __construct(
        public string $name,
        public string $email,
        public int $age,
    ) {}

    public static function fromArray(array $data): self
    {
        return new self(
            name: $data['name'] ?? throw new ValidationException('Name required'),
            email: $data['email'] ?? throw new ValidationException('Email required'),
            age: (int) ($data['age'] ?? throw new ValidationException('Age required')),
        );
    }
}

// Usage
$data = json_decode($input, true);
$request = CreateUserRequest::fromArray($data);

Disable Phar Wrapper

// In php.ini
; phar.readonly = 1  (prevents phar creation)

// At runtime - remove phar wrapper
stream_wrapper_unregister('phar');

// Validate file type before operations
if (pathinfo($file, PATHINFO_EXTENSION) === 'phar') {
    throw new SecurityException('Phar files not allowed');
}

Severity Classification

Pattern Severity CWE
unserialize($_GET/$_POST) 🔴 Critical CWE-502
unserialize without allowed_classes 🔴 Critical CWE-502
Phar with user-controlled path 🔴 Critical CWE-502
Classes with dangerous __destruct 🟠 Major CWE-502
Cache/DB deserialization 🟠 Major CWE-502
Missing signature verification 🟡 Minor CWE-502

Output Format

### Insecure Deserialization: [Description]

**Severity:** 🔴 Critical
**Location:** `file.php:line`
**CWE:** CWE-502 (Deserialization of Untrusted Data)

**Issue:**
User-controlled data is deserialized without restrictions, allowing object injection.

**Attack Vector:**
1. Attacker crafts serialized payload with gadget chain
2. Payload triggers __destruct() that writes to file
3. Attacker achieves remote code execution

**Code:**
```php
// Vulnerable
$data = unserialize($_POST['data']);

Fix:

// Secure: Use JSON or restrict classes
$data = json_decode($_POST['data'], true);

// Or with signature and whitelist
$data = unserialize($payload, [
    'allowed_classes' => [SafeDTO::class],
]);

References:

Weekly Installs
1
GitHub Stars
39
First Seen
Feb 11, 2026
Security Audits
Installed on
opencode1
claude-code1