XSS Defense: URL vs HTML Encoding
Rendering raw URL parameters into DOM nodes introduces reflected XSS vulnerabilities. A common misconception is that applying encodeURI makes a URL immune to XSS. In reality, different DOM output contexts (HTML text, attributes, JS variables, href sinks) require distinct encoding rules.
1. Problem: the misconception that URL encoding replaces XSS defense
Thinking 'full URL encoding replaces XSS escaping' is dangerous. For instance, if an attacker supplies javascript:alert(1) as a link, even after applying encodeURI, when inserted into an <a href="..."> tag, clicking it triggers the javascript: pseudo-protocol. URL encoding preserves URI syntax; it does not sanitize XSS payloads.
2. Minimal reproduction: pseudo-protocol injection and attribute boundary breakouts
The HTML snippet below shows how relying solely on URL encoding fails across different DOM output sinks:
<!-- Defect 1: Pseudo-protocol injection (valid URL encoding, executed in href Sink) -->
<a href="javascript:alert(document.cookie)">Claim Reward</a>
<!-- Defect 2: HTML attribute context breakout (missing HTML entity escaping) -->
<input type="text" value="https://example.com?a=1" onfocus="alert(1)">3. Root cause: URL parsing, protocol policy, and output encoding are separate
Template systems handle contextual escaping for HTML text and attributes, while a URL sink also requires validation of the parsed protocol. Calling encodeURI on a complete URL does not remove javascript:. Conversely, replacing & with & before assigning a DOM property changes the actual query string. With Vue :href, pass a protocol-validated ordinary URL string and let Vue handle attribute escaping.
4. Recommendation: parse and whitelist protocols, then bind through the framework
1. Parse with new URL(raw, trustedBase) and allow only http: and https: for general links. 2. Accept internal paths only when they begin with one slash and remain same-origin after parsing, rejecting protocol-relative and backslash variants. 3. Bind with Vue/Nuxt :href; do not construct HTML or use v-html. DOMPurify is for rich HTML that must be rendered, not for URL protocol validation.
5. Complete code: protocol-safe URL values for Vue :href
The function returns an ordinary URL string or null; it deliberately performs no HTML entity replacement. Render the link only when the result is non-null and pass it through property binding.
function safeHref(raw: string, trustedBase: string): string | null {
const value = raw.trim();
const isRootRelative = /^\/(?![\\/])/.test(value);
const isAbsolute = /^[a-z][a-z\d+.-]*:/i.test(value);
if (!isRootRelative && !isAbsolute) return null;
try {
const base = new URL(trustedBase);
const url = new URL(value, base);
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
if (isRootRelative) {
if (url.origin !== base.origin) return null;
return url.pathname + url.search + url.hash;
}
return url.href;
} catch {
return null;
}
}
const base = "https://jsonutils.com";
console.log(safeHref("javascript:alert(1)", base)); // null
console.log(safeHref("//evil.example/path", base)); // null
console.log(safeHref("/articles/example?a=1&b=2", base)); // preserves the real ampersand
console.log(safeHref("https://example.com/search?q=hello", base));6. Incorrect approaches
Assuming encodeURI automatically protects against all XSS attacks. Using v-html to render unvalidated URL strings. Concatenating raw URL parameters directly inside JavaScript blocks without Unicode escaping.
7. Boundaries: data:text/html protocols and inline SVG script tags
Both data:image/svg+xml and data:text/html URLs can execute XSS scripts. Protocol whitelists must strictly disallow data: URLs for general links.
8. Verification
Test case and whitespace variants of javascript:, data:, and vbscript:, plus protocol-relative URLs, backslashes, control characters, valid HTTPS, and internal paths. Component tests should inspect the final DOM property and verify no v-html path exists; complement them with CSP and browser XSS tests.
9. FAQ
Why is encodeURI('javascript:alert(1)') still unsafe? encodeURI preserves letters and parentheses, leaving a valid javascript: pseudo-protocol that browsers execute in hrefs.
Do Vue/React templates protect href XSS automatically? No! Framework escaping prevents HTML attribute breakouts but cannot validate href protocol safety.
Can HTML entity escaping and URL encoding replace each other? No! HTML entity escaping processes HTML text/attributes, while URL encoding processes URI components.
10. Summary
URL encoding maintains URI syntax integrity but cannot replace XSS security defenses. When rendering dynamic URLs into the DOM, enforce protocol whitelist checks paired with context-aware entity encoding.
Sources and further reading
Specifications and references for checking the article’s technical details.
Related articles
encodeURI vs encodeURIComponent: When to Use Each
Compare JavaScript encodeURI vs encodeURIComponent for RFC 3986 reserved characters like ?, =, /, &, detailing rules for full URLs, paths, and queries.
TroubleshootingURL Query Encoding: Plus Signs, Spaces and %2B
Analyze how traditional application/x-www-form-urlencoded specs convert spaces to + causing plus signs to parse as spaces, detailing %20 vs %2B.
PitfallsReDoS: Catastrophic Regex Backtracking
Examine Regular Expression Denial of Service (ReDoS) mechanics, detailing NFA engine catastrophic backtracking and offering safe rewriting & timeout safeguards.
Keep exploring
Use the related browser tool to test the workflow with your own sample data.
Open related tool