Only Filtering One Instance of a Special Element

Description

Only Filtering One Instance of a Special Element is an input validation vulnerability where software filters only a single occurrence of a dangerous special element, leaving additional instances unfiltered. This typically occurs when developers use non-global string replacement functions or regular expressions without global matching flags. The first (or in some cases, last) instance of the dangerous sequence is removed, but subsequent instances pass through the filter unchanged, allowing attackers to bypass security controls with multiple copies of the malicious sequence.

Risk

This vulnerability allows straightforward bypass of security controls. Attackers simply include one extra copy of the filtered sequence, knowing the filter will remove one but leave another. For path traversal, "../../file" becomes "../file" after filtering the first "../"—still allowing traversal. For XSS, "<

Solution

Always use global replacement when filtering dangerous elements. In JavaScript, use replace(/pattern/g, '') with the g flag. In Perl, use s/pattern//g. In Python, use re.sub() which is global by default. Loop replacement operations until no more matches are found to handle deeply nested patterns. Better yet, use allowlist validation that permits only known-safe characters. Use established sanitization libraries that handle these edge cases. Test filters with inputs containing 2, 3, or more instances of filtered patterns in various configurations.

Common Consequences

ImpactDetails
IntegrityScope: Integrity

Unexpected State - Additional unfiltered instances cause unintended application behavior.
ConfidentialityScope: Confidentiality

Read Application Data - Multiple traversal sequences enable file access after single removal.
IntegrityScope: Integrity

Execute Unauthorized Code - Remaining special elements enable injection attacks.

Example Code

Vulnerable Code

# Vulnerable: Missing /g modifier
my $Username = GetUntrustedInput();

# Vulnerable: 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: No 'g' flag in regex
function vulnerableFilter(userInput) {
    // Vulnerable: Only first match is replaced
    return userInput.replace(/<script>/i, '');
}

// Attack: "<<script>script>alert(1)</script>"
// After filter: "<script>alert(1)</script>" - XSS succeeds!
# Vulnerable: String replace without loop
def vulnerable_filter_traversal(path):
    # Python's str.replace() is global, but this pattern is vulnerable
    # because it doesn't handle nested patterns
    return path.replace('../', '')

# Attack: "....//....//etc/passwd"
# After filter: "../../etc/passwd" - traversal still works!
# (Each "../" is replaced, but "...." and "//" recombine into "../")
// Vulnerable: replaceFirst instead of replaceAll
public String vulnerableFilter(String input) {
    // Vulnerable: Only replaces first occurrence
    return input.replaceFirst("\\.\\./", "");
}

// Attack: "../../../etc/passwd"
// Result: "../../etc/passwd"
// Vulnerable: str_replace appears global but doesn't handle nesting
<?php
function vulnerable_filter($input) {
    // str_replace IS global, but...
    return str_replace("../", "", $input);
}

// Attack: "....//etc/passwd"
// After filter: "../etc/passwd"
// The "...." becomes ".." and "//" becomes "/" after replacement

Fixed Code

# Fixed: Use /g modifier for global replacement
my $Username = GetUntrustedInput();

# Fixed: /g replaces ALL occurrences
$Username =~ s/\.\.\///g;

# Even better: Loop until stable for nested patterns
my $prev;
do {
    $prev = $Username;
    $Username =~ s/\.\.\///g;
} while ($Username ne $prev);

# Best: Allowlist validation
if ($Username =~ /[^a-zA-Z0-9_-]/) {
    die "Invalid characters in username";
}

my $filename = "/home/user/" . $Username;
// Fixed: Use 'g' flag for global replacement
function fixedFilter(userInput) {
    // Fixed: 'g' flag for all matches, 'i' for case-insensitive
    return userInput.replace(/<script>/gi, '').replace(/<\/script>/gi, '');
}

// Better: Loop until stable
function fixedFilterLoop(userInput) {
    const pattern = /<script[^>]*>|<\/script>/gi;
    let result = userInput;
    let prev;
    do {
        prev = result;
        result = result.replace(pattern, '');
    } while (result !== prev);
    return result;
}

// Best: Use proper HTML sanitizer
const DOMPurify = require('dompurify');
function bestFilter(userInput) {
    return DOMPurify.sanitize(userInput);
}
# Fixed: Loop until no more changes
def fixed_filter_traversal(path):
    prev = None
    while path != prev:
        prev = path
        path = path.replace('../', '').replace('..\\', '')
    return path

# Better: Use pathlib for canonical path validation
from pathlib import Path

def fixed_validate_path(base_dir, user_path):
    base = Path(base_dir).resolve()
    target = (base / user_path).resolve()

    # Ensure target is under base directory
    if not str(target).startswith(str(base) + '/'):
        raise ValueError("Path traversal detected")
    return target
// Fixed: Use replaceAll for global replacement
public String fixedFilter(String input) {
    // Fixed: replaceAll is global
    String result = input;
    String prev;
    do {
        prev = result;
        result = result.replaceAll("\\.\\./", "")
                       .replaceAll("\\.\\.\\\\", "");
    } while (!result.equals(prev));
    return result;
}

// Better: Canonical path validation
public Path fixedValidatePath(Path base, String userInput) throws IOException {
    Path resolved = base.resolve(userInput).toRealPath();
    if (!resolved.startsWith(base.toRealPath())) {
        throw new SecurityException("Path traversal detected");
    }
    return resolved;
}
// Fixed: Loop until stable
<?php
function fixed_filter($input) {
    do {
        $prev = $input;
        $input = str_replace("../", "", $input);
        $input = str_replace("..\\", "", $input);
    } while ($input !== $prev);

    return $input;
}

// Better: Validate canonical path
function fixed_validate_path($base, $userPath) {
    $real = realpath($base . '/' . $userPath);
    $baseReal = realpath($base);

    if ($real === false || strpos($real, $baseReal . DIRECTORY_SEPARATOR) !== 0) {
        throw new Exception("Path traversal detected");
    }

    return $real;
}

  • CWE-792: Incomplete Filtering of One or More Instances of Special Elements (parent)
  • CWE-794: Incomplete Filtering of Multiple Instances of Special Elements (sibling)
  • CWE-23: Relative Path Traversal (related exploitation)

References

  1. MITRE Corporation. "CWE-793: Only Filtering One Instance of a Special Element." https://cwe.mitre.org/data/definitions/793.html
  2. MDN Web Docs. "String.prototype.replace()." https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace
  3. OWASP. "Path Traversal." https://owasp.org/www-community/attacks/Path_Traversal