FrontendAtlas
Interview Questions
Home>JavaScript interview questions> JavaScript coding challenges >Sanitize href URL (Block javascript

Sanitize href URL (Block javascript: XSS)

intermediate
By FrontendAtlas Editorial · Updated Jul 14, 2026
Implement sanitizeHrefUrl(input) for an <a href="..."> allowlist: explicit http, https, mailto, and tel URLs plus relative URLs that stay on a trusted origin. Return null for empty or malformed input, unknown schemes, protocol-relative URLs, ASCII controls (raw or percent-encoded), or any backslash. Reject dangerous input instead of repairing it.

Arguments

  • input: string — User-provided URL-ish string that you want to place into href.

Returns

string | null — A safe href string (normalized) or null if the input is unsafe.
Examples
sanitizeHrefUrl('javascript:alert(1)') // => null

sanitizeHrefUrl('  https://example.com  ') // => 'https://example.com/'

sanitizeHrefUrl('/settings') // => '/settings'

sanitizeHrefUrl('mailto:hi@example.com') // => 'mailto:hi@example.com'

Solution

Overview

Define the href contract before parsing: explicit http, https, mailto, and tel URLs are allowed; relative URLs must resolve to a reserved trusted origin; everything else returns null. Reject controls and backslashes before trimming or parsing so normalization cannot expose a dangerous scheme or reinterpret a relative path as another host.

1

Approach: Reject ambiguous input, then parse against an explicit policy

Steps:

1) Convert input to text, but reject raw ASCII controls and backslashes before trimming.

2) Trim outer whitespace and reject empty text, percent-encoded ASCII controls, and protocol-relative forms.

3) Detect whether the text begins with an explicit scheme.

4) Parse explicit URLs directly; resolve other text against a reserved HTTPS base.

5) Validate the final parsed protocol against http, https, mailto, and tel.

6) For relative input, also require the parsed origin and protocol to match the trusted base.

7) Return normalized href for explicit URLs, the trimmed spelling for valid relatives, or null on every failure.

Complexity: O(n) time and O(n) parser output space.

const ASCII_CONTROLS = /[\u0000-\u001F\u007F]/;
const ENCODED_ASCII_CONTROLS = /%(?:0[0-9A-F]|1[0-9A-F]|7F)/i;
const EXPLICIT_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*:/;
const SAFE_SCHEMES = new Set(['http:', 'https:', 'mailto:', 'tel:']);
const TRUSTED_BASE = new URL('https://frontendatlas.invalid/');

export default function sanitizeHrefUrl(input) {
  if (input == null) return null;

  const raw = String(input);
  if (ASCII_CONTROLS.test(raw) || raw.includes('\\')) return null;

  const value = raw.trim();
  if (!value || ENCODED_ASCII_CONTROLS.test(value) || value.startsWith('//')) {
    return null;
  }

  const isExplicit = EXPLICIT_SCHEME.test(value);

  try {
    const parsed = isExplicit ? new URL(value) : new URL(value, TRUSTED_BASE);
    if (!SAFE_SCHEMES.has(parsed.protocol)) return null;

    if (!isExplicit) {
      if (parsed.protocol !== TRUSTED_BASE.protocol || parsed.origin !== TRUSTED_BASE.origin) {
        return null;
      }
      return value;
    }

    return parsed.href;
  } catch {
    return null;
  }
}
const ASCII_CONTROLS = /[\u0000-\u001F\u007F]/;
const ENCODED_ASCII_CONTROLS = /%(?:0[0-9A-F]|1[0-9A-F]|7F)/i;
const EXPLICIT_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*:/;
const SAFE_SCHEMES = new Set(['http:', 'https:', 'mailto:', 'tel:']);
const TRUSTED_BASE = new URL('https://frontendatlas.invalid/');

export default function sanitizeHrefUrl(input: string): string | null {
  if (input == null) return null;

  const raw = String(input);
  if (ASCII_CONTROLS.test(raw) || raw.includes('\\')) return null;

  const value = raw.trim();
  if (!value || ENCODED_ASCII_CONTROLS.test(value) || value.startsWith('//')) {
    return null;
  }

  const isExplicit = EXPLICIT_SCHEME.test(value);

  try {
    const parsed = isExplicit ? new URL(value) : new URL(value, TRUSTED_BASE);
    if (!SAFE_SCHEMES.has(parsed.protocol)) return null;

    if (!isExplicit) {
      if (parsed.protocol !== TRUSTED_BASE.protocol || parsed.origin !== TRUSTED_BASE.origin) {
        return null;
      }
      return value;
    }

    return parsed.href;
  } catch {
    return null;
  }
}

Notes & Pitfalls

Pitfalls
  • Escaping HTML is not enough for href: you must also validate protocols (javascript: is the classic bypass).
  • Sanitization is context-specific: a value safe for href is not automatically safe for CSS url() or innerHTML.
  • Protocol-relative URLs and backslashes can change origin under the browser's special-URL parsing rules, so this contract rejects them.
Edge cases
  • ASCII controls are rejected rather than removed; deleting one can expose whitespace that an earlier trim already missed.
  • Percent-encoded ASCII controls and embedded backslashes are rejected anywhere in the input.
  • Empty strings should return null to avoid creating broken links.
  • Invalid http/https URLs should return null (URL() throws).
Techniques
  • Allowlist safe schemes and block everything else.
  • Reject ambiguous characters before applying ordinary outer-whitespace trimming.
  • Validate the final parsed scheme, and enforce a trusted origin for every relative URL.

Resources

  • OWASP – XSS Prevention Cheat Sheet
  • MDN – URL
  • WHATWG – URL Standard

Similar questions

Escape HTML for Safe TexteasyValidate Username (Rules + Edge Cases)easyFormat Date in Time Zone (YYYY-MM-DD)intermediate

Guides

Frontend interview preparation guideGuideFrontend coding interview questions and prep guideBlueprintJavaScript Problems That Actually Show UpBlueprintBuild Great UI in 60 MinutesBlueprint

Preparing for interviews? Use Frontend Coding Challenges first, then move into a concrete Study Plan before targeted Company Prep.

Open frontend interview questionsBrowse JavaScript interview questionsOpen Essential 60Open Machine Coding HubOpen Frontend Coding ChallengesOpen System DesignOpen Interview Prep GuideOpen System Design BlueprintOpen Framework Prep PathsOpen Study PlansOpen JavaScript mastery study planOpen Company PrepOpen JavaScript Framework Prep Guide
↗Incidents hub
← Prev←28 / 88Next →→