Originally published on Medium
Fixing one security gap without understanding the side effects can open another one.
In my last article I wrote about URL encoding and why PHP security checks often miss encoded attack payloads. A developer read it and asked a question worth exploring carefully.
“If a single urldecode() gets beaten by double encoding, then looping the decode until it stops changing feels like the obvious next move — but doesn’t that open you up to a decode bomb where an attacker sends deeply nested encoding just to burn CPU?”
It is a valid concern worth exploring in depth.
What I Mean by a “Decode Bomb”
I am using the term decode bomb to describe a specific scenario: an attacker crafts a request with deeply nested URL encoding specifically to make your server perform excessive decoding work on every request.
It is similar in concept to a ZIP bomb where a small compressed file expands into something enormous but applied to URL decoding instead of file decompression. It is not a widely standardized security term, but the pattern it describes is real. Here is how it works in practice. If your code loops urldecode() until the string stops changing, an attacker can send a deeply nested encoded payload.
%25252525252527
Here is what happens on each pass:
Pass 1: %25252525252527 → %252525252527
Pass 2: %252525252527 → %2525252527
Pass 3: %2525252527 → %25252527
Pass 4: %25252527 → %252527
Pass 5: %252527 → %2527
Pass 6: %2527 → %27
Pass 7: %27 → '
Seven passes to decode one character. Your server performs unnecessary decoding work on every request like this. In practice, the impact depends on your implementation. A single request is unlikely to cause problems, but repeated requests against code that performs excessive decoding can unnecessarily consume CPU time. You fixed the encoding blind spot. You potentially opened a resource issue in the same line of code.
Why This Is Easy to Miss
This is not the kind of vulnerability that shows up in standard penetration tests. It is the kind that gets discovered in production when someone notices their server is running slower than expected under unusual traffic patterns.
The attacker does not need to extract data. They do not need to bypass authentication. They just need your server to spend more time processing their requests than it spends serving legitimate users.
For a solo developer or small agency running client sites with limited server resources, sustained unusual traffic patterns like this can degrade performance noticeably.
The Safe Solution: Cap the Decode Passes
The fix requires a deliberate decision: choose a maximum number of decode passes and never exceed it.
$query = $_SERVER['QUERY_STRING'] ?? '';
$maxPasses = 3;
$i = 0;
$previous = null;
while ($previous !== $query && $i < $maxPasses) {
$previous = $query;
$query = urldecode($query);
$i++;
}
$query = strtolower($query);
Three passes is a reasonable upper bound in many applications, but the right limit depends on your architecture, framework, and threat model. The loop also stops early if the string stops changing so a single-encoded payload only runs one pass, not three.
Single encoding is the most common attack evasion technique. Double encoding is the next layer. Beyond that, deeply nested encoding is in territory that your web server, reverse proxy, or PHP framework should already be handling upstream before the request reaches your application code.
An Important Note Before You Add This
Before adding custom decoding logic to your application, understand how your web server, reverse proxy, framework, and PHP itself already normalize incoming requests. Multiple layers of decoding can introduce both security and correctness issues.
If you are using Laravel, remember that request data is already normalized in various ways before your application uses it. Understand where URL decoding has already occurred before adding another decoding layer.
Understand your full request stack before adding normalization code.
Why Pattern Matching Has Limits
Even with capped URL decoding, pattern matching has a fundamental weakness: it only catches what you know to look for.
Attackers constantly develop new payloads specifically designed to bypass known patterns. They use alternative syntax. They use database-specific encoding. They use comments to break up keywords.
SE/*comment*/LECT * FR/*comment*/OM users
Your pattern matching looks for select. It never appears as a contiguous string. The check may pass. Whether the attack succeeds depends on how the application processes the input afterward.
This is the core limitation of signature-based detection: it catches known bad patterns. It misses unknown ones.
The Broader Lesson: Every Security Fix Has Side Effects
This is the part most security tutorials skip.
When you add a security control, you are not just closing one door. You are potentially opening another. The question to ask after every security fix is not just “does this stop the attack I was worried about?” It is “what does this enable that I was not worried about before?”
Unlimited URL decoding stops encoding-based evasion. It introduces resource exhaustion risk.
Capped URL decoding reduces most encoding-based evasion. It limits CPU exposure. It misses deeply nested encoding which upstream infrastructure should handle anyway.
Every security decision is a tradeoff. Understanding the tradeoff is what separates a developer who adds security theater from one who builds genuine defense.
Detection vs Prevention
Both of the articles I have written so far are about detection logging suspicious requests, normalizing input, identifying behavioral signals.
Detection is valuable. But it has a ceiling.
The real fix for SQL injection lives at the query layer, not the detection layer. Parameterized queries make SQL injection structurally impossible for that query no amount of encoding tricks around it.
Detection tells you something suspicious happened. Prevention stops it from mattering.
The strongest security posture uses both:
Prevention: parameterized queries, input validation, output encoding, proper authentication
Detection: normalized pattern matching, behavioral anomaly detection, response size monitoring, real-time alerting
Neither replaces the other. Prevention reduces your attack surface. Detection covers what prevention misses.
The Direction Behind Kriosa
One of the design principles behind Kriosa is controlled input normalization keeping normalization predictable, bounded, and combined with behavioral analysis instead of relying solely on repeated decoding.
The XAI dashboard then explains what was detected and why so you understand the attack pattern, not just that something was blocked.
That is the difference between a log filter and a security layer. One tells you something looked suspicious. The other tells you what it was, why it was flagged, and what the attacker was trying to accomplish.
Try it free: kriosa.com.
Install it: composer require kriosa-ai/kriosa-php
Built by a developer from Cameroon, for developers who want to understand their security not just outsource it.
Kriosa — Sleep better we’re awake.
Top comments (0)