home / skills / masanao-ohba / claude-manifests / security-patterns

security-patterns skill

/skills/php/security-patterns

This skill provides PHP security best practices and patterns to prevent common vulnerabilities across projects.

npx playbooks add skill masanao-ohba/claude-manifests --skill security-patterns

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
10.6 KB
---
name: security-patterns
description: PHP security best practices and patterns for preventing common vulnerabilities
---

# PHP Security Patterns

Language-level security patterns for PHP, applicable to any PHP project.

## Input Validation

### Type Validation

```php
// Always validate and sanitize input
public function processUserId(mixed $input): int
{
    if (!is_numeric($input)) {
        throw new \InvalidArgumentException('User ID must be numeric');
    }

    $id = (int)$input;

    if ($id <= 0) {
        throw new \InvalidArgumentException('User ID must be positive');
    }

    return $id;
}
```

### Email Validation

```php
public function validateEmail(string $email): bool
{
    // Use built-in filter
    $filtered = filter_var($email, FILTER_VALIDATE_EMAIL);

    if ($filtered === false) {
        throw new \InvalidArgumentException('Invalid email format');
    }

    return true;
}
```

### URL Validation

```php
public function validateUrl(string $url): bool
{
    $filtered = filter_var($url, FILTER_VALIDATE_URL);

    if ($filtered === false) {
        throw new \InvalidArgumentException('Invalid URL format');
    }

    // Additional checks
    $parsed = parse_url($url);

    // Only allow http/https
    if (!in_array($parsed['scheme'] ?? '', ['http', 'https'])) {
        throw new \InvalidArgumentException('URL must use http or https');
    }

    return true;
}
```

## SQL Injection Prevention

### Use ORM Query Builder (Recommended)

```php
// ✅ CORRECT: Use ORM (CakePHP example)
public function findUserByEmail(string $email): ?User
{
    return $this->Users->find()
        ->where(['email' => $email])  // Automatically parameterized
        ->first();
}

// ✅ CORRECT: ORM with conditions array
public function findActiveUsers(int $companyId): array
{
    return $this->Users->find()
        ->where([
            'company_id' => $companyId,
            'status' => 1,
            'del_flg' => 0,
        ])
        ->toArray();
}

// ❌ WRONG: Raw SQL with string concatenation
public function findUserUnsafe(string $email): ?User
{
    $query = "SELECT * FROM users WHERE email = '" . $email . "'";  // VULNERABLE!
    return $this->getConnection()->query($query)->fetch();
}
```

### Use Query Expressions for Complex Conditions

```php
// ✅ CORRECT: Use Query expressions for complex logic
public function findUsersWithComplexCondition(string $searchTerm): array
{
    return $this->Users->find()
        ->where(function ($exp, $q) use ($searchTerm) {
            return $exp->or([
                'email LIKE' => '%' . $searchTerm . '%',
                'name LIKE' => '%' . $searchTerm . '%',
            ]);
        })
        ->toArray();
}
```

### Only Use Raw SQL When Absolutely Necessary

```php
// If raw SQL is unavoidable, ALWAYS use parameter binding
public function executeRawQuery(int $userId): array
{
    $conn = $this->getConnection();

    // ✅ CORRECT: Named parameters
    $stmt = $conn->execute(
        'SELECT * FROM users WHERE id = :id',
        ['id' => $userId],
        ['id' => 'integer']
    );

    return $stmt->fetchAll();
}

// ❌ WRONG: Never concatenate user input
public function unsafeRawQuery(string $email): array
{
    $sql = "SELECT * FROM users WHERE email = '$email'";  // VULNERABLE!
    return $this->getConnection()->execute($sql)->fetchAll();
}
```

## XSS (Cross-Site Scripting) Prevention

### Output Escaping

```php
// ✅ CORRECT: Escape output
echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');

// For HTML attributes
echo '<input value="' . htmlspecialchars($value, ENT_QUOTES, 'UTF-8') . '">';

// For JavaScript
echo '<script>var name = "' . json_encode($name, JSON_HEX_TAG | JSON_HEX_AMP) . '";</script>';

// For URLs
echo '<a href="' . urlencode($url) . '">Link</a>';
```

### Content Security Policy

```php
// Set CSP headers
header("Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'");
```

## CSRF (Cross-Site Request Forgery) Prevention

### Token Generation

```php
class CsrfToken
{
    public static function generate(): string
    {
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }

        $token = bin2hex(random_bytes(32));
        $_SESSION['csrf_token'] = $token;

        return $token;
    }

    public static function validate(string $token): bool
    {
        if (session_status() === PHP_SESSION_NONE) {
            session_start();
        }

        return isset($_SESSION['csrf_token']) && hash_equals($_SESSION['csrf_token'], $token);
    }
}
```

### Form Implementation

```php
// In form
<form method="POST">
    <input type="hidden" name="csrf_token" value="<?= CsrfToken::generate() ?>">
    <!-- Other fields -->
</form>

// In handler
if (!CsrfToken::validate($_POST['csrf_token'] ?? '')) {
    throw new SecurityException('Invalid CSRF token');
}
```

## Password Security

### Hashing

```php
// ✅ CORRECT: Use password_hash()
public function hashPassword(string $password): string
{
    return password_hash($password, PASSWORD_ARGON2ID, [
        'memory_cost' => 65536,
        'time_cost' => 4,
        'threads' => 3,
    ]);
}

// ❌ WRONG: Never use md5, sha1, or plain text
public function insecureHash(string $password): string
{
    return md5($password);  // VULNERABLE!
}
```

### Verification

```php
public function verifyPassword(string $password, string $hash): bool
{
    return password_verify($password, $hash);
}

public function needsRehash(string $hash): bool
{
    return password_needs_rehash($hash, PASSWORD_ARGON2ID, [
        'memory_cost' => 65536,
        'time_cost' => 4,
        'threads' => 3,
    ]);
}
```

## Session Security

### Secure Session Configuration

```php
// Configure secure sessions
ini_set('session.cookie_httponly', '1');
ini_set('session.cookie_secure', '1');  // HTTPS only
ini_set('session.cookie_samesite', 'Strict');
ini_set('session.use_strict_mode', '1');

// Regenerate session ID on login
session_start();
if ($loginSuccessful) {
    session_regenerate_id(true);
}
```

### Session Fixation Prevention

```php
public function login(string $username, string $password): bool
{
    if ($this->authenticate($username, $password)) {
        // Regenerate session ID to prevent fixation
        session_regenerate_id(true);

        $_SESSION['user_id'] = $userId;
        $_SESSION['last_activity'] = time();

        return true;
    }

    return false;
}
```

### Session Timeout

```php
public function checkSessionTimeout(): bool
{
    $timeout = 1800; // 30 minutes

    if (isset($_SESSION['last_activity'])) {
        if (time() - $_SESSION['last_activity'] > $timeout) {
            session_destroy();
            return false;
        }
    }

    $_SESSION['last_activity'] = time();
    return true;
}
```

## File Upload Security

### Validation

```php
public function validateUpload(array $file): bool
{
    // Check for upload errors
    if ($file['error'] !== UPLOAD_ERR_OK) {
        throw new \RuntimeException('Upload failed');
    }

    // Validate MIME type
    $allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
    $finfo = finfo_open(FILEINFO_MIME_TYPE);
    $mimeType = finfo_file($finfo, $file['tmp_name']);
    finfo_close($finfo);

    if (!in_array($mimeType, $allowedTypes)) {
        throw new \InvalidArgumentException('Invalid file type');
    }

    // Validate file size (max 5MB)
    if ($file['size'] > 5 * 1024 * 1024) {
        throw new \InvalidArgumentException('File too large');
    }

    return true;
}

public function saveUpload(array $file): string
{
    $this->validateUpload($file);

    // Generate safe filename
    $extension = pathinfo($file['name'], PATHINFO_EXTENSION);
    $filename = bin2hex(random_bytes(16)) . '.' . $extension;

    // Save outside web root
    $uploadDir = '/var/uploads/';
    $destination = $uploadDir . $filename;

    if (!move_uploaded_file($file['tmp_name'], $destination)) {
        throw new \RuntimeException('Failed to save file');
    }

    return $filename;
}
```

## Directory Traversal Prevention

### Path Sanitization

```php
public function getSecurePath(string $userPath): string
{
    // Define safe base directory
    $baseDir = realpath('/var/www/uploads/');

    // Resolve user path
    $requestedPath = realpath($baseDir . '/' . $userPath);

    // Ensure path is within base directory
    if ($requestedPath === false || strpos($requestedPath, $baseDir) !== 0) {
        throw new \InvalidArgumentException('Invalid path');
    }

    return $requestedPath;
}
```

## Command Injection Prevention

### Avoid shell execution

```php
// ✅ CORRECT: Use native PHP functions
$files = scandir($directory);

// ✅ CORRECT: If shell needed, use escapeshellarg()
$filename = escapeshellarg($userFilename);
exec("ls -la " . $filename, $output);

// ❌ WRONG: Direct shell execution with user input
exec("ls -la " . $userFilename);  // VULNERABLE!
```

## XML External Entity (XXE) Prevention

### Disable external entities

```php
// ✅ CORRECT: Disable external entity loading
libxml_disable_entity_loader(true);

$dom = new DOMDocument();
$dom->loadXML($xmlString, LIBXML_NOENT | LIBXML_DTDLOAD);

// Or use SimpleXML
$xml = simplexml_load_string($xmlString, 'SimpleXMLElement', LIBXML_NOENT);
```

## Cryptographic Random Numbers

### Use secure random

```php
// ✅ CORRECT: Use random_bytes() or random_int()
$token = bin2hex(random_bytes(32));
$randomNumber = random_int(1, 100);

// ❌ WRONG: Never use rand() or mt_rand() for security
$insecureToken = mt_rand();  // VULNERABLE!
```

## HTTP Headers

### Security Headers

```php
// X-Frame-Options
header('X-Frame-Options: SAMEORIGIN');

// X-Content-Type-Options
header('X-Content-Type-Options: nosniff');

// X-XSS-Protection
header('X-XSS-Protection: 1; mode=block');

// Strict-Transport-Security (HTTPS only)
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');

// Referrer-Policy
header('Referrer-Policy: strict-origin-when-cross-origin');
```

## Error Handling

### Don't expose sensitive information

```php
// ✅ CORRECT: Log detailed errors, show generic message
try {
    $this->processPayment($amount);
} catch (\Exception $e) {
    // Log full error for debugging
    error_log('Payment error: ' . $e->getMessage());

    // Show generic message to user
    throw new \RuntimeException('Payment processing failed');
}

// ❌ WRONG: Expose stack traces in production
ini_set('display_errors', '1');  // VULNERABLE in production!
```

## Framework-Agnostic

These security patterns apply to:
- CakePHP projects
- Laravel projects
- Symfony projects
- Any PHP application

Framework-specific security features (Authentication, Authorization, CSRF middleware, etc.) should be defined in framework-level skills (e.g., `php-cakephp/security-patterns`).

Overview

This skill presents concise, language-level PHP security best practices and patterns to prevent common vulnerabilities across any PHP project. It covers input validation, SQL injection mitigation, XSS/CSRF defenses, secure sessions, file handling, cryptography, and secure headers. Use these patterns to harden application code regardless of framework.

How this skill works

The skill inspects common attack surfaces and prescribes small, testable patterns: validate and sanitize inputs, prefer parameterized queries or ORMs, always escape output, generate and validate CSRF tokens, and configure secure sessions and headers. It provides code-level examples and rules of thumb for safe file uploads, path handling, random number generation, and error handling. These are framework-agnostic guidelines you can drop into controllers, services, or utilities.

When to use it

  • When accepting any user input (forms, APIs, files).
  • Before building or refactoring database access layers.
  • When rendering user-controlled data in HTML, attributes, JavaScript, or URLs.
  • On authentication and session-related flows (login, logout, session timeout).
  • When handling file uploads, shell calls, or XML processing.

Best practices

  • Validate types and formats early; fail fast with clear exceptions.
  • Use ORM/query builders or parameter binding; avoid raw SQL concatenation.
  • Escape output with appropriate context functions (htmlspecialchars, json_encode, urlencode).
  • Use secure password hashing (password_hash with Argon2id) and verify/rehash as needed.
  • Store sessions securely (httponly, secure, samesite), regenerate IDs on login, and enforce timeouts.
  • Use cryptographically secure random APIs (random_bytes, random_int) and set strict HTTP security headers.

Example use cases

  • API input validation: convert and validate numeric IDs and email fields before processing.
  • Data access: replace string-concatenated SQL with ORM queries or prepared statements with bound parameters.
  • Forms: embed a generated CSRF token in POST forms and validate it on submit.
  • File upload service: validate MIME type, size, generate a safe filename, and store files outside the web root.
  • Session handling: configure secure cookie flags, regenerate session IDs on login, and implement inactivity timeouts.

FAQ

Is this tied to a specific PHP framework?

No. The patterns are framework-agnostic and apply to CakePHP, Laravel, Symfony, or plain PHP projects.

What should I do if I must run shell commands?

Avoid when possible. If necessary, never concatenate user input; use escapeshellarg() and validate filenames first.

How do I prevent XXE when parsing XML?

Disable external entity loading and avoid loading external DTDs. Use libxml_disable_entity_loader or secure parser options.

Which random functions are safe for tokens?

Use random_bytes() or random_int(). Do not use rand() or mt_rand() for security-sensitive values.