This weakness occurs when a developer creates an allowlist a set of "approved" inputs to protect against malicious data, but the allowlist is too broad or…
This weakness occurs when a developer creates an allowlist (a set of "approved" inputs) to protect against malicious data, but the allowlist is too broad or poorly defined. As a result, dangerous input that should have been blocked passes through the filter. It's a false sense of security — the allowlist exists, but it doesn't actually prevent the attacks it was meant to stop.
02How It Happens
Allowlists are a sound security principle: explicitly define what *is* allowed, rather than trying to block everything that *isn't*. However, this approach fails when the allowlist is defined too loosely. Common causes include:
- Overly broad patterns. A regex or string match that was meant to be restrictive but accidentally matches harmful input (e.g., allowing any URL starting with http without checking the domain).
- Incomplete threat modeling. The developer didn't anticipate all the dangerous input forms that could slip through (e.g., allowing file extensions .jpg but not .jpeg, missing .jpe).
- Case sensitivity or encoding bypasses. The allowlist checks for lowercase .exe but an attacker uploads .EXE or a URL-encoded variant.
- Trusting user-supplied metadata. Allowing input based on a user-provided MIME type or file extension without validating the actual content.
The result is that an attacker can craft input that technically matches the allowlist but still achieves their goal — uploading malware, injecting code, or accessing restricted resources.
03Real-World Impact
A permissive allowlist can lead to serious breaches. For example, if a file upload form allows "image files" but the allowlist only checks the extension, an attacker might upload a PHP file renamed to .jpg.php or exploit double-extension handling. If an allowlist for redirect URLs allows any domain containing a trusted domain name, an attacker might redirect users to trusted-domain.attacker.com. In both cases, the allowlist *exists* but fails to prevent the attack.
04Vulnerable & Fixed Patterns
Vulnerable pattern
import re
def validate_file_extension(filename):
# Allowlist: allow common image extensions
allowed_extensions = r'\.(jpg|png|gif)$'
if re.search(allowed_extensions, filename, re.IGNORECASE):
return True
return False
# Attacker uploads "malware.php.jpg" — matches the allowlist
uploaded_file = "malware.php.jpg"
if validate_file_extension(uploaded_file):
save_file(uploaded_file) # Dangerous!
Why it's vulnerable: The regex matches any filename ending in .jpg, .png, or .gif, but doesn't validate the actual file content or prevent double extensions. An attacker can upload malware.php.jpg and, depending on server configuration, execute it as PHP.
Fixed pattern
import os
from pathlib import Path
def validate_file_extension(filename):
# Allowlist: only the final extension matters
allowed_extensions = {'.jpg', '.png', '.gif'}
# Get only the final extension
file_ext = Path(filename).suffix.lower()
return file_ext in allowed_extensions
def validate_file_content(file_bytes):
# Check magic bytes (file signature) to confirm type
magic_bytes = {
b'\xff\xd8\xff': 'jpg',
b'\x89PNG': 'png',
b'GIF8': 'gif',
}
for sig, ftype in magic_bytes.items():
if file_bytes.startswith(sig):
return ftype
return None
uploaded_file = "image.jpg"
file_content = read_uploaded_file(uploaded_file)
if validate_file_extension(uploaded_file) and validate_file_content(file_content):
save_file(uploaded_file)
Vulnerable pattern
<?php
// Allowlist: allow common image MIME types
$allowed_types = array('image/jpeg', 'image/png', 'image/gif');
if (in_array($_FILES['upload']['type'], $allowed_types)) {
move_uploaded_file($_FILES['upload']['tmp_name'],
'/uploads/' . $_FILES['upload']['name']);
} else {
echo "File type not allowed";
}
?>
Why it's vulnerable: The MIME type is supplied by the client and can be spoofed. An attacker can upload a PHP file and set its MIME type to image/jpeg to bypass the allowlist.
Fixed pattern
<?php
// Allowlist: check file extension and validate content
$allowed_extensions = array('jpg', 'jpeg', 'png', 'gif');
$file_info = pathinfo($_FILES['upload']['name']);
$file_ext = strtolower($file_info['extension']);
// Check extension against allowlist
if (!in_array($file_ext, $allowed_extensions)) {
die("File extension not allowed");
}
// Validate actual file content using getimagesize()
$image_info = getimagesize($_FILES['upload']['tmp_name']);
if ($image_info === false) {
die("File is not a valid image");
}
// Only then move the file
$safe_filename = bin2hex(random_bytes(16)) . '.' . $file_ext;
move_uploaded_file($_FILES['upload']['tmp_name'],
'/uploads/' . $safe_filename);
?>
05Prevention Checklist
Define allowlists by content, not metadata. Validate the actual data (magic bytes, structure, behavior) rather than relying on user-supplied filenames, MIME types, or extensions.
Use exact matching, not patterns. If you must use regex or wildcards, test them thoroughly against edge cases (case variations, encoding tricks, double extensions).
Combine multiple validation layers. Check extension *and* file content *and* size. No single check is foolproof.
Deny by default. If input doesn't match the allowlist, reject it immediately. Don't fall back to a blacklist or assume it's safe.
Document the threat model. Write down what attacks the allowlist is meant to prevent, and test against those specific scenarios.
Review allowlists regularly. As new attack techniques emerge, revisit your allowlist definitions to ensure they're still restrictive enough.
06Signs You May Already Be Affected
Look for unexpected file types in upload directories (e.g., .php or .exe files mixed with images), or unusual file names that suggest double extensions or encoding tricks. Check web server logs for requests to uploaded files with suspicious extensions, or for errors indicating that files were executed when they should have been served as static content. Review access logs for patterns suggesting an attacker tested multiple file upload variations.