Incomplete Filtering of Special Elements
Description
Incomplete Filtering of Special Elements is an input validation vulnerability where software receives data from an upstream component but does not completely filter special elements before sending it to a downstream component. While some filtering may be implemented, it fails to address all special characters, all instances of special characters, or all variations of special character encoding. This partial filtering provides a false sense of security while still allowing malicious input to reach the downstream component, where it may be interpreted as commands, markup, or control sequences.
Risk
Incomplete filtering is particularly dangerous because it suggests that developers were aware of the security concern but implemented an inadequate solution. Attackers can bypass incomplete filters through various techniques: using multiple instances of filtered sequences, encoding special characters differently, exploiting case sensitivity, or combining filtered elements in ways that survive partial removal. The vulnerability enables injection attacks including path traversal, command injection, SQL injection, and cross-site scripting. Because some filtering exists, the vulnerability may escape detection during basic security testing.
Solution
Implement comprehensive filtering that addresses all instances and variations of dangerous elements. Use global matching flags (like /g in regular expressions) to replace all occurrences. Loop filtering operations until no more matches are found to handle nested patterns. Consider using allowlist validation instead of denylist filtering—accept only known-safe characters rather than trying to block known-bad ones. Use well-tested input validation libraries rather than custom implementations. Prefer output encoding appropriate for the target context over input filtering. Test filtering implementations with various bypass techniques including double encoding, case variations, and recursive patterns.
Common Consequences
| Impact | Details |
|---|---|
| Integrity | Scope: Integrity Unexpected State - Unfiltered special elements cause application to reach unintended states or execute unintended operations. |
| Confidentiality | Scope: Confidentiality Read Application Data - Path traversal through incomplete filtering exposes sensitive files. |
| Integrity | Scope: Integrity Execute Unauthorized Code - Injection attacks become possible when filtering fails to neutralize all dangerous elements. |
Example Code
Vulnerable Code
# Vulnerable: Single replacement instead of global
my $Username = GetUntrustedInput();
# Vulnerable: Missing /g modifier - only removes FIRST "../"
$Username =~ s/\.\.\///;
my $filename = "/home/user/" . $Username;
ReadAndSendFile($filename);
# Attack: Input "../../../etc/passwd"
# After single replacement: "../../etc/passwd"
# Full path: "/home/user/../../etc/passwd" -> "/etc/passwd"
// Vulnerable: Only filters specific tag, not all variations
<?php
function vulnerable_filter_xss($input) {
// Vulnerable: Only removes lowercase <script> tags
$filtered = str_replace("<script>", "", $input);
$filtered = str_replace("</script>", "", $filtered);
return $filtered;
}
// Bypasses:
// <SCRIPT>alert(1)</SCRIPT> - case variation
// <scr<script>ipt>alert(1)</scr</script>ipt> - nested
// <img onerror=alert(1) src=x> - different tag
# Vulnerable: Only filters one type of path separator
def vulnerable_filter_path(path):
# Vulnerable: Only filters forward slash traversal
filtered = path.replace('../', '')
return filtered
# Bypass: "..\\..\\etc\\passwd" on Windows
# Or mixed: "..\\/etc/passwd"
// Vulnerable: Filters some SQL metacharacters but not all
public String vulnerableFilterSQL(String input) {
// Vulnerable: Only escapes single quote
String filtered = input.replace("'", "''");
return filtered;
}
// Bypasses:
// Using double quotes: "value" OR "1"="1
// Using backslash: \'; DROP TABLE users; --
// Using encoded characters in some databases
// Vulnerable: Only checks for one null byte encoding
char* vulnerable_filter_null(char* input) {
char* result = malloc(strlen(input) + 1);
int j = 0;
for (int i = 0; input[i] != '\0'; i++) {
// Vulnerable: Doesn't filter %00 or \\0 encodings
if (input[i] != 0x00) {
result[j++] = input[i];
}
}
result[j] = '\0';
return result;
}
// Bypass: "valid.txt%00.exe" - %00 not filtered
Fixed Code
# Fixed: Global replacement with recursive filtering
my $Username = GetUntrustedInput();
# Fixed: Use /g for global replacement
# And loop until stable to handle nested sequences
my $prev;
do {
$prev = $Username;
$Username =~ s/\.\.[\\/]//g; # Handle both / and \
$Username =~ s/%2e%2e[%2f%5c]//gi; # Handle URL-encoded
} while ($Username ne $prev);
# Better: Allowlist validation
if ($Username !~ /^[a-zA-Z0-9_.-]+$/) {
die "Invalid username";
}
my $filename = "/home/user/" . $Username;
// Fixed: Comprehensive HTML filtering
<?php
function fixed_filter_xss($input) {
// Fixed: Use proper HTML encoding
return htmlspecialchars($input, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
// Or use allowlist for specific allowed tags
function fixed_filter_allowed_tags($input) {
// Strip all tags except allowed ones
return strip_tags($input, '<b><i><u><p><br>');
}
// Best: Use a proper HTML sanitizer library
use HTMLPurifier;
function fixed_sanitize_html($input) {
$config = HTMLPurifier_Config::createDefault();
$purifier = new HTMLPurifier($config);
return $purifier->purify($input);
}
# Fixed: Filter all path separator variations
import os
import re
def fixed_filter_path(path):
# Fixed: Filter all variations recursively
prev = None
while path != prev:
prev = path
# Filter Unix and Windows separators
path = re.sub(r'\.\.[\\/]', '', path)
# Filter URL-encoded versions
path = re.sub(r'%2e%2e[%2f%5c]', '', path, flags=re.IGNORECASE)
path = re.sub(r'%252e%252e[%252f%255c]', '', path, flags=re.IGNORECASE)
return path
# Best: Validate canonical path
def fixed_validate_path(base_dir, user_path):
full_path = os.path.realpath(os.path.join(base_dir, user_path))
if not full_path.startswith(os.path.realpath(base_dir) + os.sep):
raise ValueError("Path traversal detected")
return full_path
// Fixed: Use parameterized queries instead of filtering
public void fixedQuery(String username) {
// Fixed: No need to filter - use prepared statement
String sql = "SELECT * FROM users WHERE name = ?";
PreparedStatement stmt = connection.prepareStatement(sql);
stmt.setString(1, username);
ResultSet rs = stmt.executeQuery();
}
// If filtering is needed, use comprehensive approach
public String fixedFilterSQL(String input) {
// Use database-specific escape function
// Or implement comprehensive escaping
StringBuilder result = new StringBuilder();
for (char c : input.toCharArray()) {
switch (c) {
case '\'': result.append("''"); break;
case '"': result.append("\\\""); break;
case '\\': result.append("\\\\"); break;
case '\0': break; // Remove null bytes
default: result.append(c);
}
}
return result.toString();
}
Related CWEs
- CWE-790: Improper Filtering of Special Elements (parent)
- CWE-792: Incomplete Filtering of One or More Instances of Special Elements (child)
- CWE-795: Only Filtering Special Elements at a Specified Location (child)
References
- MITRE Corporation. "CWE-791: Incomplete Filtering of Special Elements." https://cwe.mitre.org/data/definitions/791.html
- OWASP. "Input Validation Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html
- OWASP. "Testing for Filter Bypass." OWASP Testing Guide.