Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Untrusted output: escape by context and constrain scripts

Last updated: 5 Oct 20266 min read
tutorial
IntermediateBy AITrove Editorial

A user-submitted inspection note is data, even if it contains angle brackets or script-looking text. Rendering it with innerHTML asks the browser to parse it as markup. Rendering it with textContent keeps those bytes as text. Server-rendered templates must also escape values for their specific output context; HTML text, attributes, URLs, and script literals do not share one universal escaping rule. A Content Security Policy can limit which scripts execute, but it is defense in depth rather than permission to insert unsafe HTML. The browser example below creates a note element and assigns textContent. It also avoids constructing a link from unchecked input. Rich text needs a deliberately designed sanitizer and supported markup policy.

Case study

A submitted note contains '<img src=x onerror=alert(1)>'. On a case page it should appear as the literal note, not create an image element or run code. The DOM code places it inside a paragraph as text. If the same value is inserted into a server template's attribute, it must be escaped for an attribute context instead. A strict script policy can reduce the impact of some injection mistakes, but a policy that allows unsafe inline script defeats much of that protection. Review both the data path and the script policy rather than declaring either alone sufficient.

Working contract

javascript
function showInspectionNote(noteText, notesList) {
  if (typeof noteText !== "string") throw new TypeError("Expected note text");
  const noteItem = document.createElement("li");
  noteItem.textContent = noteText;
  notesList.append(noteItem);
}
const notesList = document.querySelector("#inspection-notes");
showInspectionNote("<img src=x onerror=alert(1)>", notesList);

Cost and tradeoffs

Assigning textContent copies O(L) characters for note length L and creates one DOM node. Sanitizing allowed rich text has greater parsing and maintenance cost and should be chosen only if the product needs rich text. A restrictive script policy can require changes to inline scripts and third-party integrations, so start with an inventory and test the policy before enforcing it. Client-side escaping does not repair unsafe server-rendered templates or stored content already emitted in the wrong context. Keep the trust boundary explicit at every render point.

Common Mistakes

  • Do not put untrusted note text into innerHTML.
  • Do not apply HTML-text escaping to a JavaScript or URL context and assume safety.
  • Do not call a permissive script policy a complete injection defense.
  • Do not build navigation targets from unchecked user strings.

Continue through the stack

Form submission: validate on the server and return field errors; Sessions and CSRF: keep identity on the server; HTML document skeleton: declare language, encoding, and a real title.

Failure trace

A case title contains markup-like text. A developer inserts it through innerHTML, turning a stored value into browser instructions; adding a restrictive policy later reduces some impact but does not make the unsafe insertion correct. Use a text sink for text, context-appropriate escaping for generated HTML, and a policy that limits script execution as another layer.

Verification

  • Store text containing angle brackets and quotes, then verify it renders as text in every view.
  • Exercise an attribute and a URL context separately; do not reuse an HTML-text escape as a universal rule.
  • Deploy the policy in a reporting stage and inspect violations before enforcing it.

Decision note

The cheapest defense is to avoid parsing untrusted strings as markup. A content policy can reduce damage and reveal mistakes, but maintaining nonces and allowed resources has operational cost.

Further connections

Embedding and Browser Capability Headers; DOM Sinks and Trusted Content.

web-tech
web-development
Storage details