Back to Curriculum

Security Best Practices

📚 Lesson 12 of 12 ⏱️ 50 min

Security Best Practices

50 min

Security is paramount in web application development. PHP applications are frequent targets for attacks, making security awareness and best practices essential. Common vulnerabilities include SQL injection (malicious SQL in queries), XSS (Cross-Site Scripting, malicious scripts in output), CSRF (Cross-Site Request Forgery, unauthorized actions), file upload vulnerabilities, and authentication flaws. Understanding these threats and how to prevent them is crucial for building secure applications.

SQL injection occurs when user input is directly inserted into SQL queries without sanitization. Attackers can manipulate queries to access, modify, or delete data. Prevention requires using prepared statements (PDO or mysqli) that separate SQL structure from data. Never concatenate user input directly into SQL queries. Always use parameterized queries with placeholders. Understanding SQL injection helps you prevent one of the most common and dangerous vulnerabilities.

XSS (Cross-Site Scripting) occurs when user input containing JavaScript is output without escaping. Attackers can steal cookies, session data, or perform actions on behalf of users. Prevention requires escaping all output using `htmlspecialchars()` or `htmlentities()`. Use context-appropriate escaping (HTML, JavaScript, URL, CSS). Content Security Policy (CSP) headers provide additional protection. Understanding XSS helps you prevent script injection attacks.

CSRF (Cross-Site Request Forgery) tricks users into performing actions they didn't intend. Attackers create requests that appear legitimate but perform unauthorized actions. Prevention requires CSRF tokens—unique tokens included in forms and validated on submission. Tokens should be unpredictable, tied to sessions, and validated on every state-changing request. Understanding CSRF helps you prevent unauthorized actions.

Authentication and authorization are crucial for protecting resources. Passwords should be hashed using `password_hash()` (never plain text or MD5). Use strong password requirements and consider rate limiting for login attempts. Sessions should be secure (HTTPS, secure cookies, session regeneration). Authorization checks should verify users have permission for actions. Understanding authentication and authorization helps you protect user accounts and resources.

Best practices include validating and sanitizing all input, escaping all output, using prepared statements for database queries, implementing CSRF protection, using secure password hashing, enabling HTTPS, keeping PHP and dependencies updated, following the principle of least privilege, and logging security events. Security is an ongoing process, not a one-time task. Understanding security best practices enables you to build applications that protect user data and resist attacks.

Key Concepts

  • Security is crucial for protecting user data and preventing attacks.
  • SQL injection is prevented by using prepared statements.
  • XSS is prevented by escaping all output.
  • CSRF is prevented by using CSRF tokens.
  • Authentication requires secure password hashing and session management.

Learning Objectives

Master

  • Preventing SQL injection with prepared statements
  • Preventing XSS by escaping output
  • Implementing CSRF protection with tokens
  • Securing authentication and authorization

Develop

  • Security thinking and threat awareness
  • Understanding common vulnerabilities and defenses
  • Designing secure, resilient applications

Tips

  • Always use prepared statements for database queries with user input.
  • Escape all output with htmlspecialchars() to prevent XSS.
  • Implement CSRF tokens for all state-changing requests.
  • Use password_hash() for password storage, never plain text.

Common Pitfalls

  • Not using prepared statements, creating SQL injection vulnerabilities.
  • Not escaping output, allowing XSS attacks.
  • Not implementing CSRF protection, allowing unauthorized actions.
  • Storing passwords in plain text or using weak hashing.

Summary

  • Security is essential for protecting user data and preventing attacks.
  • SQL injection is prevented with prepared statements.
  • XSS is prevented by escaping output.
  • CSRF is prevented with CSRF tokens.
  • Understanding security enables building secure applications.

Exercise

Implement security measures to protect against common web vulnerabilities.

<?php
// Security utility class
class Security {
    // CSRF token generation and validation
    public static function generateCSRFToken() {
        if (empty($_SESSION['csrf_token'])) {
            $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
        }
        return $_SESSION['csrf_token'];
    }
    
    public static function validateCSRFToken($token) {
        return isset($_SESSION['csrf_token']) && 
               hash_equals($_SESSION['csrf_token'], $token);
    }
    
    // Input sanitization
    public static function sanitizeInput($input, $type = 'string') {
        switch ($type) {
            case 'email':
                return filter_var(trim($input), FILTER_SANITIZE_EMAIL);
            case 'int':
                return filter_var($input, FILTER_SANITIZE_NUMBER_INT);
            case 'url':
                return filter_var(trim($input), FILTER_SANITIZE_URL);
            case 'string':
            default:
                return htmlspecialchars(trim($input), ENT_QUOTES, 'UTF-8');
        }
    }
    
    // Output escaping
    public static function escapeOutput($data) {
        if (is_array($data)) {
            return array_map([self::class, 'escapeOutput'], $data);
        }
        return htmlspecialchars($data, ENT_QUOTES, 'UTF-8');
    }
    
    // Password hashing
    public static function hashPassword($password) {
        return password_hash($password, PASSWORD_ARGON2ID);
    }
    
    public static function verifyPassword($password, $hash) {
        return password_verify($password, $hash);
    }
    
    // File upload security
    public static function validateFileUpload($file, $allowedTypes = [], $maxSize = 5242880) {
        $errors = [];
        
        if ($file['error'] !== UPLOAD_ERR_OK) {
            $errors[] = 'File upload failed';
            return $errors;
        }
        
        if ($file['size'] > $maxSize) {
            $errors[] = 'File too large';
        }
        
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $mimeType = finfo_file($finfo, $file['tmp_name']);
        finfo_close($finfo);
        
        if (!empty($allowedTypes) && !in_array($mimeType, $allowedTypes)) {
            $errors[] = 'File type not allowed';
        }
        
        return $errors;
    }
}

// Secure form processing
class SecureForm {
    private $errors = [];
    
    public function processRegistration($data) {
        // Validate CSRF token
        if (!Security::validateCSRFToken($data['csrf_token'] ?? '')) {
            $this->errors[] = 'Invalid CSRF token';
            return false;
        }
        
        // Sanitize inputs
        $name = Security::sanitizeInput($data['name'] ?? '');
        $email = Security::sanitizeInput($data['email'] ?? '', 'email');
        $password = $data['password'] ?? '';
        
        // Validate inputs
        if (empty($name) || strlen($name) < 2) {
            $this->errors[] = 'Name must be at least 2 characters';
        }
        
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            $this->errors[] = 'Invalid email format';
        }
        
        if (strlen($password) < 8) {
            $this->errors[] = 'Password must be at least 8 characters';
        }
        
        if (empty($this->errors)) {
            // Hash password
            $hashedPassword = Security::hashPassword($password);
            
            // Save to database (with prepared statements)
            return $this->saveUser($name, $email, $hashedPassword);
        }
        
        return false;
    }
    
    private function saveUser($name, $email, $hashedPassword) {
        try {
            $pdo = new PDO("mysql:host=localhost;dbname=secure_app", "user", "pass");
            $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            
            $stmt = $pdo->prepare("INSERT INTO users (name, email, password) VALUES (?, ?, ?)");
            return $stmt->execute([$name, $email, $hashedPassword]);
        } catch (PDOException $e) {
            $this->errors[] = 'Database error occurred';
            return false;
        }
    }
    
    public function getErrors() {
        return $this->errors;
    }
}

// Secure authentication
class SecureAuth {
    public function login($email, $password) {
        try {
            $pdo = new PDO("mysql:host=localhost;dbname=secure_app", "user", "pass");
            $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            
            $stmt = $pdo->prepare("SELECT id, name, email, password FROM users WHERE email = ?");
            $stmt->execute([$email]);
            $user = $stmt->fetch(PDO::FETCH_ASSOC);
            
            if ($user && Security::verifyPassword($password, $user['password'])) {
                // Regenerate session ID to prevent session fixation
                session_regenerate_id(true);
                
                $_SESSION['user_id'] = $user['id'];
                $_SESSION['user_name'] = $user['name'];
                $_SESSION['user_email'] = $user['email'];
                
                return true;
            }
            
            return false;
        } catch (PDOException $e) {
            error_log("Login error: " . $e->getMessage());
            return false;
        }
    }
    
    public function isLoggedIn() {
        return isset($_SESSION['user_id']);
    }
    
    public function logout() {
        session_destroy();
        return true;
    }
}

// Usage example
session_start();

$form = new SecureForm();
$auth = new SecureAuth();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if ($form->processRegistration($_POST)) {
        echo "Registration successful!";
    } else {
        echo "Registration failed: " . implode(', ', $form->getErrors());
    }
}

Code Editor

Output