Double URL Encoding — What It Is, Why It Happens, and How to Prevent It
Double URL encoding happens when an already-encoded URL is encoded again, turning %20 into %2520. Learn how to detect it, prevent it in APIs and proxies, and why attackers...
Double encoding happens when %20 (encoded space) gets encoded again to %2520 (% → %25, so %25 + 20 = %2520). It’s a common bug in proxy chains and a known security bypass technique.
Encode and decode URL components with the URL Encoder.
How double encoding happens
Step 1: User input "hello world"
Step 2: First encoding: "hello%20world" (%20 = space)
Step 3: Second encoding: "hello%2520world" (%25 = %, encoded again)
The server decodes once: "hello%20world" (still encoded!)
The application sees: "%20" as literal text, not a space
Or from the attacker’s perspective:
URL path: /admin/../../etc/passwd
Step 1: Encoded: /admin/%2E%2E/%2E%2E/etc/passwd
Step 2: WAF blocks %2E%2E (encoded dots in traversal)
Double-encode step: /admin/%252E%252E/%252E%252E/etc/passwd
WAF sees %252E%252E → allows through (doesn't recognize as traversal)
Server decodes once: /admin/%2E%2E/%2E%2E/etc/passwd
Second decode: /admin/../../etc/passwd
Path traversal succeeds!
Detect double encoding
// Check if a string contains percent-encoded percent signs:
function isDoubleEncoded(str) {
return /%25[0-9a-fA-F]{2}/.test(str);
// Matches: %25XX where XX is a hex pair
// %2520 → % = %25, 20 = space (was already encoded)
}
isDoubleEncoded('hello%2520world'); // true (double-encoded space)
isDoubleEncoded('hello%20world'); // false (single-encoded space)
// Decode double-encoded URL:
function safeDecodeURL(url) {
let decoded = url;
try {
// Keep decoding until stable:
while (decoded !== decodeURIComponent(decoded)) {
decoded = decodeURIComponent(decoded);
}
} catch {
return url; // Return original if decoding fails
}
return decoded;
}
Prevent double encoding in JavaScript
// ❌ Don't encode something that's already encoded:
function badEncode(value) {
const encoded = encodeURIComponent(value); // "hello%20world"
return encodeURIComponent(encoded); // "hello%2520world" — bug!
}
// ✅ Encode raw values only:
function buildURL(base, params) {
const url = new URL(base);
for (const [key, value] of Object.entries(params)) {
// URLSearchParams handles encoding correctly — never double-encodes:
url.searchParams.set(key, value); // Pass raw string
}
return url.toString();
}
buildURL('https://example.com/search', { q: 'hello world', tag: 'a&b' });
// "https://example.com/search?q=hello+world&tag=a%26b"
// ❌ Common bug in Express path parameters:
app.get('/files/:filename', (req, res) => {
const filename = req.params.filename; // Already decoded by Express
const filepath = path.join(__dirname, 'uploads', encodeURIComponent(filename));
// ❌ Double-encoding! Express decoded it, you re-encoded it.
// req.params.filename is already decoded
});
// ✅ Use the decoded value directly:
app.get('/files/:filename', (req, res) => {
const filename = req.params.filename; // "hello world.pdf" (already decoded)
const filepath = path.join(__dirname, 'uploads', filename);
// ✅ path.join handles the raw filename correctly
});
Double encoding as a security bypass
Double encoding is used in path traversal, XSS, and SQL injection bypass attacks:
Attack: GET /files/%252e%252e%252f%252e%252e%252fetc/passwd
↑ %25 = %, %2e = .
After first decode: %2e%2e%2f%2e%2e%2f
After second decode: ../../
WAF pattern matching for "../": didn't match %252e%252e%252f
Application decoded twice: ../../../etc/passwd
Defense: Always normalize and decode URLs fully before applying security checks. Use well-tested security middleware rather than manual pattern matching.
// Security: normalize before checking:
function normalizeAndValidatePath(urlPath) {
// Decode until stable:
let decoded = urlPath;
for (let i = 0; i < 10; i++) {
const next = decodeURIComponent(decoded);
if (next === decoded) break;
decoded = next;
}
// Now resolve path traversal:
const resolved = path.posix.normalize(decoded);
// Ensure path stays within allowed directory:
const ALLOWED = '/uploads/';
if (!resolved.startsWith(ALLOWED)) {
throw new Error('Path traversal detected');
}
return resolved;
}
Related tools
- URL Encoder — encode/decode URL components
- URL Canonicalization — normalize URLs
- Regex Tester — test URL patterns
Related posts
- URL Encoding: The 7 Bugs That Break Your API — Every API has at least one URL-encoding bug. Here are the seven I see most — wha…
- Percent Encoding and RFC 3986 Explained — Why is `+` sometimes a space and sometimes a literal plus? Why does `%2520` show…
- URL Canonicalization — Normalize URLs for APIs, Caching, and SEO — URL canonicalization normalizes URLs to a consistent form, removing redundant pa…
- URL Encoding in JavaScript — encodeURIComponent vs encodeURI — JavaScript has two URL encoding functions: encodeURI for full URLs and encodeURI…
Related tool
Percent-encode and decode URLs per RFC 3986.
Written by Mian Ali Khalid. Part of the Dev Productivity pillar.