# **PHP Security & Best Practices Handbook**

- *For ADHD-friendly learning - Bite-sized chunks with practical examples*

---

## **📋 Quick Reference Cheat Sheet**

### **🚨 IMMEDIATE FIXES (Do these NOW)**

1. **Remove `display_errors` from production**
2. **Use password_hash()** - ALWAYS
3. **Escape ALL user input** - NO EXCEPTIONS
4. **Validate then sanitize** - IN THAT ORDER
5. **Use HTTPS** - ALWAYS

---

## **🔐 1. ERROR HANDLING - Don't Help Hackers**

### **❌ BAD (Dangerous)**

```php
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1); // LEAKS INFO!
?>
```

### **✅ GOOD (Safe)**

```php
<?php
// config.php - Environment-based settings
define('ENV', 'development'); // Change to 'production' when live

if (ENV === 'development') {
    // Show errors only locally
    error_reporting(E_ALL);
    ini_set('display_errors', 1);
} else {
    // Production: Log errors, don't show them
    error_reporting(E_ALL);
    ini_set('display_errors', 0);
    ini_set('log_errors', 1);
    ini_set('error_log', '/secure/path/errors.log');
}

// Custom error page for users
function showSafeError($message = 'An error occurred') {
    if (ENV === 'development') {
        die("<h1>Debug:</h1><pre>$message</pre>");
    } else {
        // Log the real error
        error_log("User error: " . $message);
        // Show generic message
        die("<h1>Something went wrong</h1><p>Our team has been notified.</p>");
    }
}
?>
```

---

## **🔐 2. SESSION SECURITY - Protect User Logins**

### **❌ BAD**

```php
<?php
session_start(); // That's it? Hackers love this!
?>
```

### **✅ GOOD**

```php
<?php
// session_config.php
function secureSessionStart() {
    // 1. Prevent session fixation
    session_regenerate_id(true);
    
    // 2. Set secure cookie parameters
    session_set_cookie_params([
        'lifetime' => 86400, // 24 hours
        'path' => '/',
        'domain' => $_SERVER['HTTP_HOST'],
        'secure' => true,     // HTTPS only
        'httponly' => true,   // No JavaScript access
        'samesite' => 'Strict' // Prevent CSRF
    ]);
    
    // 3. Start session
    session_start();
    
    // 4. Validate session fingerprint
    $fingerprint = md5($_SERVER['HTTP_USER_AGENT'] . $_SERVER['REMOTE_ADDR']);
    if (!isset($_SESSION['fingerprint'])) {
        $_SESSION['fingerprint'] = $fingerprint;
    } elseif ($_SESSION['fingerprint'] !== $fingerprint) {
        // Possible session hijacking
        session_regenerate_id(true);
        session_destroy();
        die('Security violation detected');
    }
    
    // 5. Auto-logout after inactivity
    $timeout = 1800; // 30 minutes
    if (isset($_SESSION['LAST_ACTIVITY']) && 
        (time() - $_SESSION['LAST_ACTIVITY']) > $timeout) {
        session_destroy();
        header('Location: login.php?timeout=1');
        exit;
    }
    $_SESSION['LAST_ACTIVITY'] = time();
}
?>
```

---

## **🔐 3. DATABASE - SQL Injection Protection**

### **❌ BAD (Suicide)**

```php
<?php
// NEVER DO THIS!
$user = $_POST['username'];
$sql = "SELECT * FROM users WHERE username = '$user'";
?>
```

### **✅ GOOD (Prepared Statements)**

```php
<?php
class SafeDatabase {
    private $pdo;
    
    public function __construct() {
        $this->pdo = new PDO(
            'mysql:host=localhost;dbname=secure_db;charset=utf8mb4',
            'username',
            'password',
            [
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
                PDO::ATTR_EMULATE_PREPARES => false // IMPORTANT!
            ]
        );
    }
    
    // Example 1: User login (Safe)
    public function getUser($username) {
        $stmt = $this->pdo->prepare("SELECT * FROM users WHERE username = ?");
        $stmt->execute([$username]);
        return $stmt->fetch();
    }
    
    // Example 2: User registration (Safe)
    public function registerUser($data) {
        $stmt = $this->pdo->prepare("
            INSERT INTO users 
            (username, email, password, curp) 
            VALUES (?, ?, ?, ?)
        ");
        
        // Hash password BEFORE storing
        $hashedPassword = password_hash($data['password'], PASSWORD_DEFAULT);
        
        return $stmt->execute([
            $data['username'],
            $data['email'],
            $hashedPassword,
            $data['curp']
        ]);
    }
    
    // Example 3: Search with LIKE (Still safe!)
    public function searchUsers($term) {
        $stmt = $this->pdo->prepare("
            SELECT * FROM users 
            WHERE username LIKE ? 
            OR email LIKE ?
        ");
        
        $searchTerm = "%$term%";
        $stmt->execute([$searchTerm, $searchTerm]);
        return $stmt->fetchAll();
    }
}
?>
```

---

## **🔐 4. PASSWORD SECURITY - No Compromises**

### **❌ BAD (All wrong)**
```php
<?php
// NEVER store passwords like this:
$password = md5($_POST['password']); // Weak
$password = sha1($_POST['password']); // Also weak
$password = base64_encode($_POST['password']); // Not encryption!
?>
```

### **✅ GOOD (Follow these rules)**
```php
<?php
class PasswordSecurity {
    
    // RULE 1: Always use password_hash()
    public static function hashPassword($plainPassword) {
        return password_hash($plainPassword, PASSWORD_DEFAULT);
    }
    
    // RULE 2: Always verify with password_verify()
    public static function verifyPassword($plainPassword, $hashedPassword) {
        return password_verify($plainPassword, $hashedPassword);
    }
    
    // RULE 3: Check if password needs rehashing
    public static function needsRehash($hashedPassword) {
        return password_needs_rehash($hashedPassword, PASSWORD_DEFAULT);
    }
    
    // RULE 4: Enforce strong passwords
    public static function validateStrength($password) {
        $errors = [];
        
        if (strlen($password) < 12) {
            $errors[] = "Minimum 12 characters";
        }
        
        if (!preg_match('/[A-Z]/', $password)) {
            $errors[] = "At least one uppercase letter";
        }
        
        if (!preg_match('/[a-z]/', $password)) {
            $errors[] = "At least one lowercase letter";
        }
        
        if (!preg_match('/[0-9]/', $password)) {
            $errors[] = "At least one number";
        }
        
        if (!preg_match('/[^A-Za-z0-9]/', $password)) {
            $errors[] = "At least one special character";
        }
        
        return $errors;
    }
}

// Usage example:
$password = $_POST['password'];

// 1. Check strength
$strengthErrors = PasswordSecurity::validateStrength($password);
if (!empty($strengthErrors)) {
    die("Weak password: " . implode(', ', $strengthErrors));
}

// 2. Hash it
$hashedPassword = PasswordSecurity::hashPassword($password);

// 3. Store $hashedPassword in database

// 4. Later, verify login
$isValid = PasswordSecurity::verifyPassword(
    $_POST['login_password'],
    $hashedPasswordFromDB
);
?>
```

---

## **🔐 5. FILE UPLOADS - Dangerous!**

### **✅ SAFE FILE UPLOAD FUNCTION**
```php
<?php
class SafeUpload {
    
    public static function uploadFile($fileInputName, $allowedTypes = []) {
        // Default allowed types (images only)
        $defaultAllowed = ['image/jpeg', 'image/png', 'image/gif'];
        $allowed = empty($allowedTypes) ? $defaultAllowed : $allowedTypes;
        
        if (!isset($_FILES[$fileInputName])) {
            throw new Exception('No file uploaded');
        }
        
        $file = $_FILES[$fileInputName];
        
        // 1. Check for upload errors
        if ($file['error'] !== UPLOAD_ERR_OK) {
            throw new Exception('Upload error: ' . $file['error']);
        }
        
        // 2. Check file size (max 5MB)
        $maxSize = 5 * 1024 * 1024;
        if ($file['size'] > $maxSize) {
            throw new Exception('File too large (max 5MB)');
        }
        
        // 3. Verify MIME type (NOT file extension!)
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mimeType = finfo_file($finfo, $file['tmp_name']);
        finfo_close($finfo);
        
        if (!in_array($mimeType, $allowed)) {
            throw new Exception('Invalid file type');
        }
        
        // 4. Generate safe filename
        $extension = self::getExtensionFromMime($mimeType);
        $safeName = uniqid('file_', true) . '.' . $extension;
        
        // 5. Define upload directory (outside web root!)
        $uploadDir = '/var/www/secure_uploads/'; // NOT in public_html!
        
        // 6. Move file with safe name
        $destination = $uploadDir . $safeName;
        
        if (!move_uploaded_file($file['tmp_name'], $destination)) {
            throw new Exception('Failed to save file');
        }
        
        return [
            'original_name' => $file['name'],
            'safe_name' => $safeName,
            'path' => $destination,
            'size' => $file['size'],
            'type' => $mimeType
        ];
    }
    
    private static function getExtensionFromMime($mimeType) {
        $mimeMap = [
            'image/jpeg' => 'jpg',
            'image/png' => 'png',
            'image/gif' => 'gif',
            'application/pdf' => 'pdf'
        ];
        return $mimeMap[$mimeType] ?? 'bin';
    }
}

// Usage:
try {
    $uploaded = SafeUpload::uploadFile('user_file');
    echo "File uploaded safely as: " . $uploaded['safe_name'];
} catch (Exception $e) {
    echo "Upload failed: " . $e->getMessage();
}
?>
```

---

## **🔐 6. INPUT VALIDATION & SANITIZATION**

### **✅ INPUT HANDLER CLASS**
```php
<?php
class SecureInput {
    
    // Validate different types of data
    public static function validate($type, $value) {
        switch ($type) {
            case 'email':
                return filter_var($value, FILTER_VALIDATE_EMAIL);
                
            case 'curp':
                // Basic CURP validation
                return preg_match('/^[A-Z]{4}[0-9]{6}[HM][A-Z]{5}[0-9A-Z]{2}$/', $value);
                
            case 'phone':
                // Mexican phone format
                return preg_match('/^(\+52|52)?[1-9][0-9]{9}$/', $value);
                
            case 'rfc':
                // Mexican RFC
                return preg_match('/^[A-Z&Ñ]{3,4}[0-9]{6}[A-Z0-9]{3}$/', $value);
                
            case 'zip':
                // Mexican ZIP code
                return preg_match('/^[0-9]{5}$/', $value);
                
            default:
                return htmlspecialchars(trim($value), ENT_QUOTES, 'UTF-8');
        }
    }
    
    // Sanitize for different contexts
    public static function sanitize($context, $value) {
        switch ($context) {
            case 'html':
                // For displaying in HTML
                return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
                
            case 'sql':
                // Use with PDO parameters instead!
                // This is just for legacy code
                return str_replace(
                    ['\\', "\0", "\n", "\r", "'", '"', "\x1a"],
                    ['\\\\', '\\0', '\\n', '\\r', "\\'", '\\"', '\\Z'],
                    $value
                );
                
            case 'url':
                return urlencode($value);
                
            case 'filename':
                // Remove dangerous characters from filenames
                return preg_replace('/[^a-zA-Z0-9._-]/', '', $value);
                
            default:
                return strip_tags($value); // Remove HTML tags
        }
    }
    
    // Complete form processing example
    public static function processForm($rules) {
        $cleanData = [];
        $errors = [];
        
        foreach ($rules as $field => $rule) {
            $value = $_POST[$field] ?? '';
            
            // 1. Required check
            if ($rule['required'] && empty($value)) {
                $errors[$field] = "Field is required";
                continue;
            }
            
            // 2. Type validation
            if (isset($rule['type'])) {
                if (!self::validate($rule['type'], $value)) {
                    $errors[$field] = "Invalid format";
                    continue;
                }
            }
            
            // 3. Length validation
            if (isset($rule['min_length']) && strlen($value) < $rule['min_length']) {
                $errors[$field] = "Too short (min {$rule['min_length']} chars)";
                continue;
            }
            
            if (isset($rule['max_length']) && strlen($value) > $rule['max_length']) {
                $errors[$field] = "Too long (max {$rule['max_length']} chars)";
                continue;
            }
            
            // 4. Sanitize
            $context = $rule['sanitize'] ?? 'html';
            $cleanData[$field] = self::sanitize($context, $value);
        }
        
        return [
            'data' => $cleanData,
            'errors' => $errors,
            'valid' => empty($errors)
        ];
    }
}

// Usage example:
$rules = [
    'username' => [
        'required' => true,
        'min_length' => 4,
        'max_length' => 20,
        'sanitize' => 'html'
    ],
    'email' => [
        'required' => true,
        'type' => 'email',
        'sanitize' => 'html'
    ],
    'curp' => [
        'required' => true,
        'type' => 'curp',
        'sanitize' => 'html'
    ],
    'phone' => [
        'required' => false,
        'type' => 'phone',
        'sanitize' => 'html'
    ]
];

$result = SecureInput::processForm($rules);

if ($result['valid']) {
    // Safe to use $result['data']
    $cleanUsername = $result['data']['username'];
    // Insert into database...
} else {
    // Show errors
    foreach ($result['errors'] as $field => $error) {
        echo "$field: $error<br>";
    }
}
?>
```

---

## **🔐 7. CSRF PROTECTION - Prevent Form Hijacking**

### **✅ SIMPLE CSRF PROTECTION**
```php
<?php
class CSRFProtection {
    
    public static function generateToken() {
        if (!isset($_SESSION['csrf_tokens'])) {
            $_SESSION['csrf_tokens'] = [];
        }
        
        $token = bin2hex(random_bytes(32));
        $_SESSION['csrf_tokens'][$token] = time();
        
        // Clean old tokens (older than 1 hour)
        foreach ($_SESSION['csrf_tokens'] as $storedToken => $timestamp) {
            if (time() - $timestamp > 3600) {
                unset($_SESSION['csrf_tokens'][$storedToken]);
            }
        }
        
        return $token;
    }
    
    public static function validateToken($token) {
        if (!isset($_SESSION['csrf_tokens'][$token])) {
            return false;
        }
        
        // Check token age (max 1 hour)
        if (time() - $_SESSION['csrf_tokens'][$token] > 3600) {
            unset($_SESSION['csrf_tokens'][$token]);
            return false;
        }
        
        // Token used, remove it (one-time use)
        unset($_SESSION['csrf_tokens'][$token]);
        return true;
    }
    
    public static function addToForm() {
        $token = self::generateToken();
        return '<input type="hidden" name="csrf_token" value="' . $token . '">';
    }
}

// Usage in form:
echo '<form method="POST">';
echo CSRFProtection::addToForm();
echo '<input type="text" name="data">';
echo '<button type="submit">Submit</button>';
echo '</form>';

// Usage in form processing:
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $token = $_POST['csrf_token'] ?? '';
    
    if (!CSRFProtection::validateToken($token)) {
        die('CSRF token validation failed!');
    }
    
    // Safe to process form
}
?>
```

---

## **🔐 8. SECURITY HEADERS - Free Protection**

### **✅ SECURE HEADERS MIDDLEWARE**
```php
<?php
class SecurityHeaders {
    
    public static function setHeaders() {
        // 1. Prevent clickjacking
        header('X-Frame-Options: DENY');
        
        // 2. Prevent MIME type sniffing
        header('X-Content-Type-Options: nosniff');
        
        // 3. Enable XSS protection (legacy browsers)
        header('X-XSS-Protection: 1; mode=block');
        
        // 4. Strict transport security (HTTPS only)
        header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
        
        // 5. Content security policy (adjust for your needs)
        header("Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:;");
        
        // 6. Referrer policy
        header('Referrer-Policy: strict-origin-when-cross-origin');
        
        // 7. Permissions policy (new)
        header('Permissions-Policy: geolocation=(), microphone=(), camera=()');
    }
}

// Use it at the start of every page:
SecurityHeaders::setHeaders();
?>
```

---

## **🔐 9. SECURITY CHECKLIST - Quick Audit**

### **📝 MONTHLY CHECKLIST**
```php
<?php
// security_audit.php - Run this monthly
class SecurityAudit {
    
    public static function runChecks() {
        $checks = [];
        
        // 1. PHP version check
        $checks['php_version'] = version_compare(PHP_VERSION, '8.1.0', '>=');
        
        // 2. display_errors check
        $checks['display_errors'] = ini_get('display_errors') == '0';
        
        // 3. Open basedir restriction
        $checks['open_basedir'] = ini_get('open_basedir') !== '';
        
        // 4. Session security
        $checks['session_cookie_httponly'] = ini_get('session.cookie_httponly') == '1';
        $checks['session_cookie_secure'] = ini_get('session.cookie_secure') == '1';
        
        // 5. File uploads
        $checks['file_uploads'] = ini_get('file_uploads') == '0' ? true : false;
        
        // 6. Dangerous functions disabled
        $disabled = explode(',', ini_get('disable_functions'));
        $dangerous = ['exec', 'system', 'passthru', 'shell_exec'];
        $checks['dangerous_functions'] = count(array_intersect($dangerous, $disabled)) == count($dangerous);
        
        return $checks;
    }
    
    public static function generateReport() {
        $checks = self::runChecks();
        $report = "<h2>Security Audit Report</h2>";
        $report .= "<table border='1'>";
        $report .= "<tr><th>Check</th><th>Status</th><th>Action</th></tr>";
        
        foreach ($checks as $check => $status) {
            $report .= "<tr>";
            $report .= "<td>" . ucwords(str_replace('_', ' ', $check)) . "</td>";
            $report .= "<td>" . ($status ? "✅ PASS" : "❌ FAIL") . "</td>";
            $report .= "<td>" . self::getAction($check, $status) . "</td>";
            $report .= "</tr>";
        }
        
        $report .= "</table>";
        return $report;
    }
    
    private static function getAction($check, $status) {
        if ($status) return "OK";
        
        $actions = [
            'php_version' => "Upgrade PHP to 8.1+",
            'display_errors' => "Set display_errors = 0 in php.ini",
            'session_cookie_httponly' => "Set session.cookie_httponly = 1",
            'file_uploads' => "Set file_uploads = Off if not needed"
        ];
        
        return $actions[$check] ?? "Check configuration";
    }
}

// Run audit and email report to admin
if ($_GET['run_audit'] ?? false) {
    $report = SecurityAudit::generateReport();
    mail('admin@yourdomain.com', 'Security Audit Report', $report);
    echo $report;
}
?>
```

---

## **🚨 EMERGENCY ACTION PLAN**

### **If you suspect a breach:**
1. **IMMEDIATELY**: Change all database passwords
2. **Check logs**: `/var/log/apache2/error.log`, auth logs
3. **Audit users**: Check for unauthorized accounts
4. **Update everything**: PHP, extensions, server OS
5. **Notify affected users** if personal data exposed

### **Daily monitoring commands:**
```bash
# Check failed logins
grep "Failed password" /var/log/auth.log

# Check web server errors
tail -100 /var/log/apache2/error.log

# Check for modified PHP files
find /var/www -name "*.php" -mtime -1

# Check disk space (full disks cause issues)
df -h
```

---

## **📚 LEARNING PATH (One chunk per week)**

**Week 1**: Error handling & logging  
**Week 2**: Session security  
**Week 3**: Prepared statements  
**Week 4**: Password hashing  
**Week 5**: File upload security  
**Week 6**: Input validation  
**Week 7**: Security headers  
**Week 8**: Regular audits  

---

## **🎯 REMEMBER**

1. **NEVER trust user input** - ALWAYS validate AND sanitize
2. **ALWAYS use HTTPS** - No exceptions
3. **NEVER display errors** to users in production
4. **ALWAYS hash passwords** with `password_hash()`
5. **NEVER write raw SQL** with user input
6. **REGULARLY update** PHP and all libraries
7. **BACKUP daily** - Test your backups monthly
8. **LOG everything** - But store logs securely

---

**You can do this!** Start with one section at a time. Copy these classes into your project and test them. Security is a journey, not a destination. Every small improvement makes you and your users safer. 💪

Want me to create a specific security module for your municipio system?