Unsafe HTML sinks and URL validation¶
Assigning untrusted text to innerHTML asks the browser to parse it as markup. If that content contains active browser behavior, it can lead to cross-site scripting. A constant, application-owned HTML fragment is a different case: the existence of an HTML sink alone does not prove attacker control.
Trust boundary: a label should be text, and a destination should satisfy a URL policy. HTML escaping and URL validation solve different problems. Escaping quotation marks does not turn a javascript: destination into a safe link. OWASP's DOM XSS guidance distinguishes output contexts and recommends appropriate DOM APIs.
Unsafe example: combine two inputs into HTML¶
container.innerHTML = "<a href='" + destination + "'>" + label + "</a>";
Both the attribute and the text position are interpreted as HTML. A vaguely named escapeHTML helper is not evidence that the URL's scheme, host, and intended use have been checked.
Safer example: a text link to one approved origin¶
This browser helper replaces the container's contents with a link. Its policy permits only HTTPS URLs on a fictional, application-controlled support origin, with no URL credentials.
function showSupportLink(container, destination, label) {
if (typeof destination !== 'string' || destination.length > 2048
|| typeof label !== 'string' || label.length < 1 || label.length > 200) {
throw new TypeError('Invalid link input');
}
const url = new URL(destination);
if (url.origin !== 'https://support.example.com' || url.username || url.password) {
throw new TypeError('Unsupported link destination');
}
const link = container.ownerDocument.createElement('a');
link.href = url.href;
link.textContent = label;
container.replaceChildren(link);
}
The URL parser provides a structured origin comparison, including scheme and port, rather than a string-prefix test. The label goes through textContent on an ordinary anchor; it never becomes HTML. URL.origin and Node.textContent document those APIs.
The approved origin must actually be controlled by your application. Consider path-level restrictions or server-owned route IDs if only a few destinations are intended. An approved site can still contain an open redirect or unsafe content, so origin validation is not a guarantee about the eventual page. This browser link policy also does not establish safe server-side fetching; SSRF prevention has additional network requirements.
If the feature only displays text, use container.textContent = value after validating the value. If rich HTML is required, define the allowed formatting and use a maintained sanitizer configured for that context; see the reviewed React/HTML guide. Do not pass arbitrary attribute names or event-handler strings through a supposedly safe DOM helper.
Regression test¶
In a local DOM fixture, accept a support URL and a label containing harmless markup-looking text; assert the anchor's text is preserved and no element is created from the label. Reject HTTP, javascript:, a lookalike hostname, a different port, URL credentials, malformed input, and overlong values. Assert rejected inputs leave the container unchanged. A unit test of the URL policy should be complemented by browser tests of the actual DOM integration.
Related: cross-site scripting, HTML injection, and open redirects.
Reference: CWE-79 — cross-site scripting.