Preventing Security Risks from Double Percent-Decoding
Discover why decoding percent-encoded data more than once creates a vulnerability allowing attackers to bypass security filters via directory traversal.
Percent-encoding replaces a single byte with a three-character sequence. This sequence starts with a percent sign followed by two hexadecimal digits. These digits range from zero to nine and uppercase A to F. It is a crude but effective method for transporting data through systems that expect only a limited set of characters.
The complexity lies in the reserved characters. A character is not simply encoded or not encoded. Its status depends entirely on its location within the URL. For example, a forward slash is essential for defining paths, so you must encode it if it appears as data within a path segment. However, that same slash might be acceptable elsewhere.
The query string follows different rules than the path. You will find that many libraries handle these distinctions inconsistently, which is likely why you are reading this now. A space may become percent two zero or it may become a plus sign depending on which part of the URL you are editing and which specification your library chooses to follow.
The double decoding trap
The real danger appears when a system decodes an input more than once. This creates a vulnerability where filters can be bypassed with ease. Consider the sequence percent two five two e.
A single pass of a decoder transforms percent two five into a literal percent sign, leaving you with percent two e. If your security filter checks the string at this stage, it sees no full stop and assumes the input is safe. It sees a percent sign and some numbers, which are generally harmless in isolation.
If the application then passes this string to another component that also performs decoding, the percent two e becomes a full stop. This allows an attacker to slip a directory traversal attack or a file extension change past your perimeter. The filter looked once, but the application processed it twice. It is a quiet failure that often goes unnoticed until a system is compromised.
This behaviour occurs frequently in complex architectures where a load balancer, a web server, and an application framework all sit in a row. If each layer decides to be helpful by decoding the URI, you have created a multi-stage pipeline for obfuscation.
Handling input in practice
You should decode your data exactly one time. This must happen at the outermost boundary of your application. Once you have transformed the encoded string into its raw form, you keep it that way throughout the rest of the request lifecycle. You do not pass an encoded string to internal functions and trust them to decode it again.
Validation must occur after this single decoding step. If you run a regular expression or a blacklist against a string before decoding it, you are inviting the double decoding problem into your codebase. The sequence of operations is critical. First you decode, then you validate, then you use. Any other order is an invitation for trouble.
Ensure you use a library that strictly adheres to RFC three nine eight six or its successors. Many home grown decoding functions forget to handle edge cases or behave unpredictably when they encounter a trailing percent sign without accompanying digits. Using a standard tool reduces the chance of these oddities causing a crash.
When you are debugging these issues, look for where the data is being transformed. If you see a string that looks partially decoded, you have found your leak. Standardise on one format and hold it there until the data reaches its final destination.
Try it: URL encoder and decoder
Sources
Every link below is checked before this page is published.