Safeguard
Security

Java URL Decode: Doing It Safely Without Opening Holes

Java URL decode looks trivial until you hit double-decoding and encoding mismatches. Here is how to decode URLs in Java correctly and where the security bugs hide.

Yukti Singhal
Security Analyst
6 min read

To decode a URL in Java, use java.net.URLDecoder.decode(value, StandardCharsets.UTF_8), but the security risk is almost never the API call itself, it is decoding the same value twice or validating it before you decode rather than after. Java URL decode is one of those operations that looks finished the moment it compiles, and that is exactly why decoding bugs slip into production. The method returns a string; whether that string is safe depends entirely on when and how many times you called it.

This guide covers the correct way to decode a URL in Java and the specific mistakes that turn a one-line utility into an injection or traversal vector.

The right way to decode a URL in Java

The workhorse is URLDecoder. Always pass the charset explicitly.

import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;

String raw = "name%3Dvalue%20with%20spaces";
String decoded = URLDecoder.decode(raw, StandardCharsets.UTF_8);
// -> "name=value with spaces"

Two details matter. First, the overload that takes a charset object exists since Java 10; before that you passed the charset name as a string, and the no-charset overload is deprecated because it relies on the platform default, which is not portable. Second, URLDecoder implements the application/x-www-form-urlencoded rules, which means it turns + into a space. That is correct for query-string form data and wrong for other parts of a URL, a distinction we will come back to.

URLDecoder is for form data, not whole URLs

A common mistake in Java URL decode code is running an entire URL through URLDecoder. It is built for decoding individual query-string components, not paths, and its +-to-space behavior will corrupt any path segment that legitimately contains a +.

If you are working with a full URL, parse it first, then decode the pieces.

import java.net.URI;

URI uri = new URI(fullUrl);
String path = uri.getPath();      // already decoded per RFC 3986
String query = uri.getRawQuery(); // still encoded; decode components yourself

URI decodes the path according to the URL specification rather than the form-encoding rules, so you avoid the + trap. Decode individual query parameters with URLDecoder only after you have split them on & and =.

The double-decoding trap

This is the security bug that actually hurts. Double decoding happens when a value is decoded, passed along, and decoded again somewhere downstream. An attacker exploits it by encoding a payload twice so that it looks harmless at the layer that validates and becomes dangerous at the layer that uses it.

Consider a path check. An attacker sends %252e%252e%252f. Your framework decodes it once to %2e%2e%2f, your validation sees no ../ and passes it, then a later URLDecoder.decode turns it into ../ right before a file read. The traversal sailed straight through the check because you validated the wrong representation.

// DANGEROUS: decoding twice defeats any validation done in between
String once = URLDecoder.decode(input, StandardCharsets.UTF_8);
// ... validation happens here ...
String twice = URLDecoder.decode(once, StandardCharsets.UTF_8); // attacker's real payload appears now

The rule: decode exactly once, as early as possible, and validate the fully decoded value. Never decode a value that a downstream layer will decode again. If you are unsure whether a framework already decoded a parameter, find out rather than "just decoding to be safe."

Validate after decoding, never before

The general principle behind the double-decoding bug generalizes to all input handling: canonicalize first, then validate. If you check a value while it is still percent-encoded, an attacker controls what your check actually sees.

For path handling specifically, decode, then resolve to a canonical absolute path, then confirm it still lives under the directory you intended.

import java.nio.file.Path;

Path base = Path.of("/srv/data").toAbsolutePath().normalize();
Path target = base.resolve(decodedUserPath).normalize();
if (!target.startsWith(base)) {
    throw new SecurityException("Path traversal attempt");
}

normalize() collapses .. segments, and the startsWith check rejects anything that escaped the base directory. This pattern only works because you decoded before normalizing; skip the decode step and an encoded .. slips past normalize() untouched. Our path traversal guide goes deeper on the file-access side.

Charset consistency across the stack

Encoding and decoding must agree on the charset. UTF-8 is the modern default and the one you should use everywhere unless a legacy system forces otherwise. A mismatch, encoding as UTF-8 and decoding as ISO-8859-1, produces mojibake at best and, at worst, lets carefully crafted multibyte sequences dodge a filter that assumed single-byte characters.

Pin the charset explicitly at every encode and decode call. Relying on the platform default means the same code behaves differently on a developer laptop and a Linux container, which is both a bug source and, occasionally, a security one when a filter's assumptions do not survive the environment change.

Where these bugs come from in real code

Decoding bugs rarely originate in code you wrote deliberately. They come from a filter or interceptor that decodes for logging, a framework that decodes path variables you then decode again, or a library buried in your dependency tree that does its own decoding with different assumptions. That transitive angle is worth a scan; an SCA tool such as Safeguard can point out where a dependency handles URL input in ways your own review would miss.

FAQ

How do I decode a URL in Java?

Use URLDecoder.decode(value, StandardCharsets.UTF_8) for query-string components, and parse a full URL with java.net.URI first so you decode each part with the right rules. Always pass the charset explicitly rather than relying on the platform default.

Why does URLDecoder turn plus signs into spaces?

Because URLDecoder implements application/x-www-form-urlencoded decoding, where + means a space. That is correct for query-string form data but wrong for URL paths, so decode path segments with URI rather than URLDecoder.

What is double-decoding and why is it dangerous?

Double-decoding is when a value gets decoded more than once across your stack. An attacker double-encodes a payload so it passes validation at one layer and becomes malicious at the next. Decode exactly once and validate the fully decoded value.

Should I validate input before or after decoding it?

After. Canonicalize the value fully, decoding and normalizing, then validate. Checking a still-encoded value lets an attacker hide traversal sequences or injection payloads behind percent-encoding that your check never sees.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.