Javascript is required
Best practicesPublished 2026-07-28Updated 2026-08-086 min read

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.

By: Tiny
XSS DefenseURL EncodeHTML EntitySecurity

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 &amp; 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

Keep exploring

Use the related browser tool to test the workflow with your own sample data.

Open related tool