Skip to content

XSS encoding pipelines: order matters more than people admit

URL encoding, HTML entities, Base64, and JavaScript escaping change meaning depending on order. This article explains the operational mistakes.

Published on 2 min read

Encoding is not decoration.

In XSS testing, encoding is part of the exploit path. If the target decodes once before rendering, a double-encoded payload may work. If it decodes after sanitization, the bug is different. If the application stores the encoded form and the frontend decodes it later, the payload might only fire on a second page.

Those distinctions are not academic. They decide whether the report is valid.

One decode is not two decodes

Take a basic payload:

<svg onload=alert(1)>

URL-encoded once:

%3Csvg%20onload%3Dalert%281%29%3E

URL-encoded twice:

%253Csvg%2520onload%253Dalert%25281%2529%253E

If the app decodes exactly once before writing into HTML, the first version becomes executable markup. The second stays encoded. If a proxy decodes once and the app decodes again, the second version can become the dangerous one.

You cannot know that from the payload alone. You need to observe the pipeline.

HTML entities have their own traps

HTML entity encoding behaves differently from URL encoding because the browser parser gets involved. &lt; in text content is usually safe as text. The same idea inside an attribute, after concatenation, may be less obvious. Numeric entities, mixed case, missing semicolons, and browser error recovery all complicate the result.

Filters often block <script> but miss equivalent parser paths:

&#x3c;svg onload=alert(1)&#x3e;

That does not mean the payload will execute everywhere. It means the next parser stage deserves attention.

Base64 is usually a transport trick

Base64 does not execute anything by itself. It matters when the application or a sink decodes it.

element.innerHTML = atob(location.hash.slice(1));

In that case, testing raw markup misses the actual path. The exploit needs to match the sink contract. This is where a generator with explicit pipeline steps is useful: build the raw payload, apply the same encoding the target expects, then preserve both versions for the report.

Mutation before encoding, usually

Most payload mutations should happen before encoding. If you split a tag, change case, inject tabs, or replace characters after URL encoding, you may produce nonsense. There are exceptions, especially when testing broken decoders or filters that operate on encoded text, but those should be deliberate choices.

A reasonable pipeline looks like this:

template -> placeholders -> mutation -> encoding -> output

When something works, record the order. Developers need that detail. "Encoded payload executed" is vague. "Input is URL-decoded after the denylist and then assigned to innerHTML" is a bug.

Related articles

Payload choice is less important than injection context. HTML body, attributes, URLs, scripts, and DOM sinks all fail differently.
A sandboxed XSS playground is useful only if it makes isolation, parser context, and execution limits explicit.
Responsible XSS testing depends on scope, evidence discipline, data minimization, and knowing when a proof of concept has gone far enough.