Only Filtering Special Elements Relative to a Marker
Description
Only Filtering Special Elements Relative to a Marker is an input validation vulnerability where software filters dangerous elements only when they appear at specific positions relative to string markers, such as the beginning (^) or end ($) of a string. This approach uses anchored pattern matching that only checks at boundary positions, allowing identical dangerous sequences to pass through when they appear elsewhere in the input. The filter assumes attackers will place malicious content at predictable locations, which is easily circumvented.
Risk
Attackers can trivially bypass marker-relative filtering by positioning dangerous sequences away from the checked marker. If a filter removes "../" only at the string beginning, inputs like "x/../../../etc/passwd" or "foo/../../../etc/passwd" pass through completely unchanged. This is particularly dangerous in path handling where any traversal sequence, regardless of position, can navigate the directory tree. The vulnerability is common when developers use regex anchors (^, $) without realizing they limit matching to specific positions. Even a single unfiltered instance of a dangerous sequence can fully compromise the intended security control.
Solution
Remove position anchors from security-critical pattern matching unless the entire string must match a specific format. Use global, unanchored patterns that find dangerous sequences anywhere in the input. For path validation, resolve inputs to canonical paths and verify they remain within allowed directories. Implement iterative filtering that continues until no more dangerous patterns are found. Prefer allowlist validation that only accepts known-safe characters. When anchors are needed for format validation, combine them with comprehensive content validation that examines the entire input.
Common Consequences
| Impact | Details |
|---|---|
| Integrity | Scope: Integrity Unexpected State - Dangerous elements positioned away from markers pass through filters unchanged. |
| Confidentiality | Scope: Confidentiality Read Application Data - Unfiltered path traversal sequences enable access to sensitive files. |
| Integrity | Scope: Integrity Execute Unauthorized Code - Injection sequences avoid filtering by appearing after checked markers. |
Example Code
Vulnerable Code
# Vulnerable: ^ anchor only matches at string start
my $Username = GetUntrustedInput();
# Vulnerable: Only removes "../" at the very beginning
$Username =~ s/^\.\.\///;
my $filename = "/home/user/" . $Username;
ReadAndSendFile($filename);
# Attack: "../../../etc/passwd" -> removes first, leaves "../../etc/passwd"
# Attack: "x/../../../etc/passwd" -> "../" not at start, unchanged
# Attack: "subdir/../../../etc/passwd" -> prefix allows bypass
# Vulnerable: Only checks if string starts with dangerous pattern
def vulnerable_filter(path):
# Vulnerable: Only removes leading traversal
if path.startswith('../'):
path = path[3:]
return path
# Attack: "foo/../../../etc/passwd" - doesn't start with "../"
# Attack: "./../../etc/passwd" - starts with "./" not "../"
// Vulnerable: Only matches at string boundaries
function vulnerableFilter(input) {
// Vulnerable: ^ anchors match only at start
return input.replace(/^\.\.\//g, '');
}
// Even with /g flag, ^ means "start of string"
// Attack: "x/../../../etc/passwd" - unchanged
// Attack: "dir/../../../etc/passwd" - unchanged
// Vulnerable: Only strips from beginning or end
<?php
function vulnerable_filter_start($input) {
// Vulnerable: Only removes from beginning
if (strpos($input, '../') === 0) {
return substr($input, 3);
}
return $input;
}
function vulnerable_filter_end($input) {
// Vulnerable: Only removes from end
if (substr($input, -3) === '../') {
return substr($input, 0, -3);
}
return $input;
}
// Attacks work when dangerous content is in the middle
// Vulnerable: Uses startsWith/endsWith checks
public String vulnerableFilter(String input) {
// Vulnerable: Only checks at boundaries
if (input.startsWith("../")) {
return input.substring(3);
}
if (input.endsWith("../")) {
return input.substring(0, input.length() - 3);
}
return input;
}
// Attack: "subdir/../../../etc/passwd"
// Attack: "../../../etc/passwd" only removes first "../"
// Vulnerable: Only checks first N characters
char* vulnerable_filter(char* input) {
// Vulnerable: Only checks if "../" at position 0
if (strncmp(input, "../", 3) == 0) {
return input + 3;
}
return input;
}
// Attack: "x/../../../etc/passwd" - "../" not at position 0
Fixed Code
# Fixed: Remove anchors, use global matching with loop
my $Username = GetUntrustedInput();
# Fixed: No ^ anchor - matches anywhere
# Loop handles nested patterns
my $prev;
do {
$prev = $Username;
$Username =~ s/\.\.[\\/]//g; # Match both separators
} while ($Username ne $prev);
# Better: Allowlist validation
unless ($Username =~ /^[a-zA-Z0-9_.-]+$/) {
die "Invalid username";
}
my $filename = "/home/user/" . $Username;
# Fixed: Check entire string, use canonical path
import os.path
def fixed_filter(path):
# Fixed: Remove from anywhere, loop until stable
prev = None
while path != prev:
prev = path
path = path.replace('../', '').replace('..\\', '')
return path
# Better: Canonical path validation
def fixed_validate_path(base_dir, user_path):
# Resolve to canonical form
full_path = os.path.realpath(os.path.join(base_dir, user_path))
base_path = os.path.realpath(base_dir)
# Verify result is under base directory
if not full_path.startswith(base_path + os.sep):
raise ValueError("Path traversal detected")
return full_path
// Fixed: No anchors, global search
function fixedFilter(input) {
// Fixed: No anchors, matches anywhere
let result = input;
let prev;
do {
prev = result;
// No ^ or $ - matches throughout string
result = result.replace(/\.\.[\\/]/g, '');
} while (result !== prev);
return result;
}
// Better: Use path module for validation
const path = require('path');
function fixedValidatePath(basePath, userInput) {
const fullPath = path.resolve(basePath, userInput);
const realBase = path.resolve(basePath);
if (!fullPath.startsWith(realBase + path.sep)) {
throw new Error('Path traversal detected');
}
return fullPath;
}
// Fixed: Check and remove from entire string
<?php
function fixed_filter($input) {
// Fixed: Loop until no more changes
do {
$prev = $input;
// Remove from anywhere in string
$input = str_replace('../', '', $input);
$input = str_replace('..\\', '', $input);
} while ($input !== $prev);
return $input;
}
// Better: Use realpath validation
function fixed_validate_path($base, $userPath) {
$basePath = realpath($base);
$fullPath = realpath($base . DIRECTORY_SEPARATOR . $userPath);
if ($fullPath === false ||
strpos($fullPath, $basePath . DIRECTORY_SEPARATOR) !== 0) {
throw new Exception("Path traversal detected");
}
return $fullPath;
}
// Fixed: Search entire string
import java.nio.file.Path;
import java.nio.file.Paths;
public String fixedFilter(String input) {
// Fixed: Remove from anywhere, loop until stable
String result = input;
String prev;
do {
prev = result;
result = result.replace("../", "").replace("..\\", "");
} while (!result.equals(prev));
return result;
}
// Better: Canonical path validation
public Path fixedValidatePath(Path base, String userInput) throws Exception {
Path resolved = base.resolve(userInput).normalize().toRealPath();
Path realBase = base.toRealPath();
if (!resolved.startsWith(realBase)) {
throw new SecurityException("Path traversal detected");
}
return resolved;
}
Related CWEs
- CWE-795: Only Filtering Special Elements at a Specified Location (parent)
- CWE-797: Only Filtering Special Elements at an Absolute Position (sibling)
- CWE-791: Incomplete Filtering of Special Elements (grandparent)
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory (related exploitation)
References
- MITRE Corporation. "CWE-796: Only Filtering Special Elements Relative to a Marker." https://cwe.mitre.org/data/definitions/796.html
- OWASP. "Path Traversal." https://owasp.org/www-community/attacks/Path_Traversal
- Regular-Expressions.info. "Anchors." https://www.regular-expressions.info/anchors.html