← Tilbage til security guides

Input Validation & Sanitization

Input validation og sanitization sikrer at al brugerinput valideres og renses før det bruges i applikationen for at forhindre injections og data corruption.

🚨Critical Severity
⚠️

Om Truslen

Input validation er fundamentet for applikationssikkerhed og den første forsvarslinje mod de fleste angreb. Princippet er simpelt men kritisk: aldrig stol på brugerinput. Al data fra brugere, eksterne APIs, filer og selv andre dele af din applikation skal behandles som potentielt ondsindet. Manglende eller utilstrækkelig input validation er roden til de fleste sikkerhedssårbarheder, inklusive SQL injection, XSS, command injection, path traversal og remote code execution. Der findes to komplementære tilgange: whitelist validation (accepter kun kendt-gode værdier) og blacklist validation (afvis kendt-dårlige værdier). Whitelist er altid at foretrække, da det er umuligt at forudse alle mulige angrebsvektorer. Validation skal altid ske på server-side, da client-side validation let kan omgås. Moderne PHP tilbyder omfattende validation og sanitization funktioner gennem filter_var(), filter_input() og relaterede funktioner.

Key Points

  • Validér ALT input på server-side - client-side validation er kun for brugervenlighed
  • Brug whitelist validation når muligt - accepter kun kendt-gode værdier
  • Brug filter_var() og filter_input() til type-sikker input validation
  • Implementer validation så tidligt som muligt i request lifecycle
  • Validér datatype, format, længde, range og allowed characters
  • Brug prepared statements til database-queries selv efter validation
  • Sanitér output baseret på kontekst (HTML, JavaScript, URL, SQL)
  • Implementer strict validation for filuploads (type, size, extension)
  • Brug regular expressions til format-validation, men pas på ReDoS
  • Validér og sanitér data fra ALLE kilder: $_GET, $_POST, $_COOKIE, $_FILES, headers
  • Implementer input-længdebegrænsninger for at reducere attack surface
  • Log validation failures for security monitoring
  • Brug type declarations og strict types i PHP for ekstra sikkerhed
  • Implementer contextual output encoding efter validation
  • Kombinér validation med andre sikkerhedsmekanismer som CSP og CORS

Sårbar Kode (UNDGÅ)

Brug ALDRIG denne kode i produktion!

<?php
// SÅRBAR KODE - Brug ALDRIG dette i produktion!

// 1. INGEN VALIDATION: Direkte brug af user input
$user_id = $_GET['id'];
$query = "SELECT * FROM users WHERE id = $user_id";
$result = mysqli_query($conn, $query);
// Sårbar for SQL injection hvis id ikke er et tal

// 2. UTILSTRÆKKELIG VALIDATION: Kun type check
$email = $_POST['email'];
if (is_string($email)) {
    $query = "INSERT INTO subscribers (email) VALUES ('$email')";
    mysqli_query($conn, $query);
}
// is_string() validerer ikke om det er en valid email
// Stadig sårbar for SQL injection

// 3. CLIENT-SIDE VALIDATION ONLY
?>
<form method="POST">
    <input type="email" name="email" required>
    <script>
        // Kun JavaScript validation - let at omgå!
        document.querySelector('form').addEventListener('submit', function(e) {
            let email = document.querySelector('input[name=email]').value;
            if (!email.includes('@')) {
                e.preventDefault();
                alert('Invalid email');
            }
        });
    </script>
</form>
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // Ingen server-side validation!
    $email = $_POST['email'];
    saveEmail($email);
}

// 4. BLACKLIST VALIDATION (utilstrækkeligt)
$username = $_POST['username'];

// Forsøger at blokere farlige tegn
if (strpos($username, "'") !== false || 
    strpos($username, '"') !== false || 
    strpos($username, '<') !== false) {
    die("Invalid characters");
}
// Umuligt at forudse alle farlige inputs
// Bedre at whitelist tilladte tegn

// 5. INGEN LÆNGDEBEGRÆNSNING
$comment = $_POST['comment'];
$query = "INSERT INTO comments (text) VALUES ('$comment')";
mysqli_query($conn, $query);
// Attacker kan sende gigantiske inputs (DoS)
// Ingen validering af længde

// 6. USIKKER FILE UPLOAD
if (isset($_FILES['upload'])) {
    $filename = $_FILES['upload']['name'];
    $destination = 'uploads/' . $filename;
    
    move_uploaded_file($_FILES['upload']['tmp_name'], $destination);
}
// MEGET FARLIGT:
// - Ingen type validation
// - Ingen størrelse check
// - Path traversal sårbar (../../etc/passwd)
// - Kan uploade PHP scripts og få remote code execution

// 7. MAGIC QUOTES (forældet og usikkert)
$input = $_POST['data'];

if (get_magic_quotes_gpc()) {
    $input = stripslashes($input);
}
// Magic quotes er fjernet i PHP 5.4+
// Var aldrig tilstrækkelig beskyttelse alligevel

// 8. REGEX VALIDATION MED ReDoS SÅRBARHED
$input = $_POST['input'];

// Dårligt regex der kan forårsage ReDoS
if (preg_match('/^(a+)+$/', $input)) {
    echo "Valid";
}
// Input som "aaaaaaaaaaaaaaaaaaaaaaaaaaaa!" kan tage meget lang tid

// 9. TYPE JUGGLING SÅRBARHED
$user_input = $_GET['admin'];

if ($user_input == true) {
    // Grant admin access
}
// "0" eller "false" evaluerer til false
// Men "anything" evaluerer til true!
// Brug === for strict comparison

// 10. INGEN SANITIZATION AF OUTPUT
$name = $_GET['name'];
echo "<h1>Welcome " . $name . "</h1>";
// Sårbar for XSS
// Input burde saniteres før output

// 11. USIKKER DESERIALIZATION
$data = $_COOKIE['user_data'];
$user = unserialize($data);
// Sårbar for object injection
// Attacker kan craft ondsindet serialized data

// 12. MANGLENDE CSRF TOKEN VALIDATION
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $amount = $_POST['amount'];
    $to_account = $_POST['to_account'];
    
    // Ingen CSRF token check!
    transferMoney($amount, $to_account);
}
?>

Sikker Kode (ANBEFALET)

Best practices for sikker implementation

<?php
// SIKKER KODE - Best practices for input validation

// 1. TYPE-SAFE VALIDATION med filter_input()
$user_id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);

if ($user_id === false || $user_id === null) {
    http_response_code(400);
    die("Invalid user ID");
}

// Brug prepared statement selv efter validation
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$user_id]);

// 2. EMAIL VALIDATION
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);

if ($email === false || $email === null) {
    http_response_code(400);
    die("Invalid email address");
}

// Ekstra validation: Check email længde
if (strlen($email) > 255) {
    die("Email too long");
}

// Verificer domain eksisterer (optional)
list($user, $domain) = explode('@', $email);
if (!checkdnsrr($domain, 'MX')) {
    die("Email domain does not exist");
}

$stmt = $pdo->prepare("INSERT INTO subscribers (email) VALUES (?)");
$stmt->execute([$email]);

// 3. WHITELIST VALIDATION for enumerated values
$allowed_sort_columns = ['name', 'price', 'created_at'];
$sort_column = $_GET['sort'] ?? 'name';

if (!in_array($sort_column, $allowed_sort_columns, true)) {
    $sort_column = 'name'; // Fallback til default
}

// Nu er det sikkert at bruge i SQL (column names kan ikke parameteriseres)
$allowed_directions = ['ASC', 'DESC'];
$direction = strtoupper($_GET['dir'] ?? 'ASC');

if (!in_array($direction, $allowed_directions, true)) {
    $direction = 'ASC';
}

$query = "SELECT * FROM products ORDER BY {$sort_column} {$direction}";
$stmt = $pdo->query($query);

// 4. COMPREHENSIVE INPUT VALIDATION CLASS
class InputValidator {
    public static function validateUsername($username) {
        // Whitelist: kun alfanumeriske tegn og underscore
        if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $username)) {
            return false;
        }
        return $username;
    }
    
    public static function validatePassword($password) {
        $errors = [];
        
        if (strlen($password) < 12) {
            $errors[] = "Minimum 12 characters";
        }
        
        if (strlen($password) > 128) {
            $errors[] = "Maximum 128 characters";
        }
        
        if (!preg_match('/[A-Z]/', $password)) {
            $errors[] = "Must contain uppercase letter";
        }
        
        if (!preg_match('/[a-z]/', $password)) {
            $errors[] = "Must contain lowercase letter";
        }
        
        if (!preg_match('/[0-9]/', $password)) {
            $errors[] = "Must contain number";
        }
        
        if (!preg_match('/[^A-Za-z0-9]/', $password)) {
            $errors[] = "Must contain special character";
        }
        
        return empty($errors) ? true : $errors;
    }
    
    public static function validateURL($url) {
        $url = filter_var($url, FILTER_VALIDATE_URL);
        
        if ($url === false) {
            return false;
        }
        
        // Whitelist allowed protocols
        $parsed = parse_url($url);
        if (!isset($parsed['scheme']) || !in_array($parsed['scheme'], ['http', 'https'], true)) {
            return false;
        }
        
        return $url;
    }
    
    public static function validateInt($value, $min = null, $max = null) {
        $int = filter_var($value, FILTER_VALIDATE_INT);
        
        if ($int === false) {
            return false;
        }
        
        if ($min !== null && $int < $min) {
            return false;
        }
        
        if ($max !== null && $int > $max) {
            return false;
        }
        
        return $int;
    }
}

// Brug validation class
$username = $_POST['username'] ?? '';
if (!InputValidator::validateUsername($username)) {
    die("Invalid username format");
}

// 5. SIKKER FILE UPLOAD VALIDATION
if (isset($_FILES['upload'])) {
    $file = $_FILES['upload'];
    
    // Check for upload errors
    if ($file['error'] !== UPLOAD_ERR_OK) {
        die("Upload error: " . $file['error']);
    }
    
    // Validate file size (max 5MB)
    $max_size = 5 * 1024 * 1024;
    if ($file['size'] > $max_size) {
        die("File too large");
    }
    
    // Validate MIME type
    $allowed_types = ['image/jpeg', 'image/png', 'image/gif'];
    $finfo = new finfo(FILEINFO_MIME_TYPE);
    $mime_type = $finfo->file($file['tmp_name']);
    
    if (!in_array($mime_type, $allowed_types, true)) {
        die("Invalid file type");
    }
    
    // Validate file extension
    $allowed_extensions = ['jpg', 'jpeg', 'png', 'gif'];
    $extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
    
    if (!in_array($extension, $allowed_extensions, true)) {
        die("Invalid file extension");
    }
    
    // Generate secure filename
    $new_filename = bin2hex(random_bytes(16)) . '.' . $extension;
    $destination = __DIR__ . '/uploads/' . $new_filename;
    
    // Ensure uploads directory is not web-accessible or disable PHP execution
    if (!move_uploaded_file($file['tmp_name'], $destination)) {
        die("Failed to save file");
    }
    
    echo "File uploaded successfully";
}

// 6. SANITIZATION baseret på kontekst
$user_input = $_POST['comment'] ?? '';

// For HTML output
$html_safe = htmlspecialchars($user_input, ENT_QUOTES | ENT_HTML5, 'UTF-8');
echo "<p>" . $html_safe . "</p>";

// For JavaScript output
$js_safe = json_encode($user_input, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
echo "<script>var comment = " . $js_safe . ";</script>";

// For URL parameter
$url_safe = urlencode($user_input);
echo "<a href='search.php?q=" . $url_safe . "'>Search</a>";

// 7. STRICT TYPES for ekstra sikkerhed
declare(strict_types=1);

function processUserId(int $user_id): void {
    // Garanterer at $user_id er integer
    global $pdo;
    $stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
    $stmt->execute([$user_id]);
}

// Dette vil kaste en TypeError hvis ikke integer
try {
    $id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
    if ($id !== false && $id !== null) {
        processUserId($id);
    }
} catch (TypeError $e) {
    error_log("Type error: " . $e->getMessage());
    http_response_code(400);
    die("Invalid input type");
}

// 8. RATE LIMITING på input
class RateLimiter {
    private $redis;
    
    public function checkLimit($identifier, $max_attempts = 10, $window = 60) {
        $key = "rate_limit:" . $identifier;
        $current = $this->redis->incr($key);
        
        if ($current === 1) {
            $this->redis->expire($key, $window);
        }
        
        return $current <= $max_attempts;
    }
}
?>

Almindelige Fejl

  • At kun implementere client-side validation uden server-side validation
  • At bruge blacklist validation i stedet for whitelist hvor det er muligt
  • At validere input efter det er brugt i stedet for før
  • At stole på HTTP headers som User-Agent eller Referer der let kan forfalskes
  • At glemme at validere data fra cookies, session og database
  • At bruge usikre funktioner som extract() eller eval() med user input

Forebyggelsesteknikker

  • 🛡️Implementer whitelist validation for alle enumerated values
  • 🛡️Brug filter_var() og filter_input() med passende filters
  • 🛡️Validér datatype, format, længde og range for alt input
  • 🛡️Implementer strict type declarations i PHP funktioner
  • 🛡️Brug prepared statements for database queries
  • 🛡️Implementer contextual output encoding baseret på hvor data bruges
  • 🛡️Validér file uploads grundigt: type, size, extension, content
  • 🛡️Implementer rate limiting på input-intensive endpoints

Testing Metoder

  • 🔍Fuzz testing med tilfældige og malformed inputs
  • 🔍Test med SQL injection payloads: ', ", OR 1=1, UNION SELECT
  • 🔍Test med XSS payloads: <script>alert(1)</script>, \"><script>
  • 🔍Test med path traversal: ../, ../../etc/passwd
  • 🔍Test med oversized inputs for at identificere DoS sårbarheder
  • 🔍Test med special characters og unicode for at finde encoding-problemer

Quick Info

Severity Level
🚨Critical
Relaterede Trusler
  • SQL Injection - Injection af ondsindet SQL gennem uvalideret input
  • XSS - Cross-Site Scripting gennem uescaped output
  • Command Injection - Injection af OS-kommandoer gennem shell functions

⚠️Sikkerhedsadvarsel

Sikkerhedssårbarheder kan have alvorlige konsekvenser. Test altid grundigt i et sikkert miljø og implementer alle anbefalede forebyggelsesteknikker.