Safeguard
Application Security•

ASVS 5.0 V1.1.1: Decode Input Once Before Validation

ASVS V1.1.1 says decode input to canonical form exactly once, before validation. Here is how double decoding breaks filters, and how to implement and test it.

Safeguard Security Team
5 min read

ASVS 5.0 requirement V1.1.1 (Level 2) says your application must decode or unescape input into its canonical form only once, only when encoded data in that form is actually expected, and before any validation or sanitization runs. Decoding after you validate, or decoding twice, lets an attacker hide a payload that your filter never saw.

Why it matters

A validator can only judge what it sees. If the check runs on %253Cscript%253E and a later step decodes it to %3Cscript%3E and then again to <script>, the filter approved something harmless looking and the application then used something dangerous. Attackers call this double encoding, and it has defeated path traversal filters, WAF rules and XSS blocklists for decades.

The same pattern shows up in three places. A web server decodes the URL path, the framework decodes it again, and your handler decodes a query parameter a third time. Or a service validates a JSON field, then passes it to a library that unescapes HTML entities. Or a message queue consumer base64 decodes a body after the producer already validated the encoded form.

The rule is simple to state: there is one point where data becomes canonical, and every check happens after it.

How to implement it

Pick a single boundary, usually the edge of your request handling, and do all decoding there. Everything past that boundary treats values as plain, already decoded data and never decodes again.

Three practical rules help:

  1. Decode only what the field is documented to carry. A username field should not be percent decoded by your code, because the framework already did that once.
  2. Never call a decode function on a value that came out of a decode function.
  3. Validate after decoding, against the canonical form, with an allowlist.

Here is a Python example for a file download endpoint that avoids the classic double decoding path traversal bug.

from pathlib import Path
from urllib.parse import unquote

BASE = Path("/srv/files").resolve()

def resolve_download(raw_name: str) -> Path:
    # raw_name is the still encoded value from the request
    name = unquote(raw_name)            # decode exactly once
    if "%" in name:
        # a second layer of encoding is present; reject instead of decoding again
        raise ValueError("double encoded input")
    candidate = (BASE / name).resolve()
    if BASE not in candidate.parents:
        raise ValueError("path escapes base directory")
    return candidate

Notice that the check for a leftover % turns a silent second decode into an explicit rejection. If your framework already decodes path parameters, do not call unquote at all. Read the framework docs to find out which layer owns decoding for each input source (path, query, form, header, cookie).

For Node and Express, the same idea applies. Express decodes route params for you, so calling decodeURIComponent on req.params.name again is a double decode.

app.get('/files/:name', (req, res) => {
  const name = req.params.name;            // already decoded once by Express
  if (name.includes('%') || name.includes('\0')) {
    return res.status(400).send('bad name');
  }
  // validate name against an allowlist next
});

How to test it

Build a small test set of layered encodings and send them to every input source.

  • %252e%252e%252f (double encoded ../)
  • %2527 (double encoded single quote)
  • %26lt%3Bscript%26gt%3B (HTML entity inside percent encoding)
  • Unicode variants such as %c0%af for overlong slashes

For each, the expected outcome is either rejection or the literal string treated as plain data. It must never behave as the decoded form. A concrete test: request /files/%252e%252e%252fetc%252fpasswd and confirm a 400 or 404, never file contents.

Also read the code. Search for every call to decode, unescape, base64 decode and entity decode functions, and check that none runs after a validation call on the same value. A dynamic scanner will catch some of this, and a static scan will catch the ordering problem when you flag validate then decode sequences.

Common mistakes

  • Validating the raw string and then letting a library decode it for you.
  • Decoding in a middleware and again in the handler because both authors wanted to be safe.
  • Normalizing Unicode after validation, so a lookalike character becomes a slash or quote later.
  • Decoding fields "just in case" when the field never carries encoded data.
  • Fixing the symptom with a blocklist for %25 instead of fixing the order.

Try it yourself

Write a tiny test harness around your decode and validate path. The goal is to prove that a value is decoded exactly once, whatever the source.

import pytest

PAYLOADS = ["%252e%252e%252f", "%2527", "%26lt%3Bscript%26gt%3B", "..%c0%af"]

@pytest.mark.parametrize("raw", PAYLOADS)
def test_no_second_decode(raw):
    with pytest.raises(ValueError):
        resolve_download(raw + "etc%252fpasswd")

Run it, then deliberately add a second unquote call and watch which cases start passing through. That failing run is a useful lesson for the team: it shows exactly which inputs a second decode would have let in.

Next, draw the path of one request on paper: reverse proxy, web server, framework router, middleware, handler, service, database layer. Mark each place that touches encoding. Most teams find two or three decoders they did not know about, and the fix is usually to remove one rather than add a check.

A last useful habit is to log the length of a value before and after decoding at your boundary during testing. If the length shrinks after you believed decoding was finished, something downstream is still decoding.

Quick check

Why is decoding after validation unsafe?

The validator approved the encoded form. Decoding afterwards creates a different value that was never checked, so a payload can pass the filter and arrive intact.

What should happen when you see a second layer of encoding?

Reject the input, or keep it as literal data. Do not decode it again silently.

Sources

Never miss an update

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

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.