Course outline

JavaScript / Advanced: scheduling and trustworthy interfaces / Lesson 17

Security boundaries in browser JavaScript

Treat external data as untrusted without confusing validation, output handling, and authorization. Design safe text and URL boundaries and explain what client-side controls cannot guarantee.

About 4 min readContent reviewed
On this page

Lesson goals

  • Identify sources, transformations, and sensitive sinks.
  • Use text and URL APIs with explicit policies.
  • Keep secrets and authorization outside client trust.

A value can be valid data and unsafe markup

Trace data from forms, URLs, storage, messages, and API responses to where it is used. Validation checks whether a value meets a domain rule; output handling controls how that value is interpreted in a specific context. A legitimate course title may contain angle brackets, so banning characters indiscriminately is not a complete security design. Prefer APIs that treat values as data, and reserve HTML parsing for an explicitly reviewed markup contract. A string safe for text insertion is not automatically safe as a URL, CSS value, script body, or authorization decision. Each sink needs its own policy.

Render untrusted labels without HTML interpretation

Use textContent for a data label and create the surrounding structure through DOM APIs. The example preserves markup-like characters while creating no child elements. This demonstrates one safe text sink, not universal sanitization. Rich HTML content requires a maintained, context-appropriate sanitizer and a carefully defined allowed feature set; hand-written replacements are easy to get wrong. Content Security Policy and Trusted Types, where supported and configured, can add defenses around certain script and DOM behaviors, but do not excuse unsafe application rendering or establish that every injection path is closed.

javascript Browser-only: a narrowly safe text sink
const label = document.createElement("span");
const externalTitle = "<em>sale</em>";
label.textContent = externalTitle;
console.log(label.textContent);
console.log(label.childElementCount);
Expected output
<em>sale</em>
0

The literal label is preserved and no em element is created. URL and markup contexts still require different policies.

Parse URLs, then enforce an allow policy

A redirect destination or resource link should be parsed with URL and checked against an explicit policy. Comparing raw prefixes can mistake a look-alike origin or unusual representation for a trusted destination. The example allows only HTTPS on one exact origin; it does not perform navigation or fetch anything. Add path restrictions if the application requires them, and decide whether relative URLs are accepted against a fixed trusted base. A safe client link still does not authorize the target operation. Validate corresponding redirect and access policies at the server boundary as well.

javascript Local URL policy; no request or navigation
function allowedDestination(input) {
  try {
    const url = new URL(input, "https://learning.example");
    return url.protocol === "https:" && url.origin === "https://learning.example";
  } catch { return false; }
}
console.log(allowedDestination("/courses"));
console.log(allowedDestination("https://other.example/courses"));
console.log(allowedDestination("javascript:alert(1)"));
Expected output
true
false
false

Parsing separates protocol and origin before comparison. The illustrative javascript URL is rejected and never executed.

Do not merge arbitrary input into privileged state

External objects can contain unexpected keys and nested shapes. Copy allowed fields into a known domain record instead of recursively merging every key into a configuration object. Prototype-related keys and inherited properties deserve particular care, and own-property checks alone do not define the whole allowed schema. Validate type, limits, and semantic relationships as well. For cross-window messages, verify origin and the expected sender relationship, then validate payload shape. Avoid evaluating strings as code through eval or Function for parsing or configuration; JSON parsing is data interpretation but still needs a domain validation step afterward.

Client behavior is not a security authority

Hiding a refund button or checking a role in JavaScript improves presentation but cannot enforce server permission. A caller can construct a different request, alter storage, or bypass the page. Keep authorization and sensitive state validation on the trusted server boundary, and never embed private service credentials in shipped JavaScript. Browser storage is reachable by script under its security model; it is not a secret vault. Test defensive behavior only in an owned isolated environment with synthetic accounts. Report scope and remaining uncertainty instead of claiming a passing scan or a few examples prove absence of vulnerabilities.

Common errors and fixes

One sanitizer is assumed safe for every sink

Why it happens
Text, URL, HTML, and code contexts are conflated.
How to fix it
Use context-specific APIs and an explicit rendering policy.
Prevent it next time
Review data flow into each sensitive sink.

A URL is allowed by a raw prefix

Why it happens
String appearance substitutes for parsed origin checks.
How to fix it
Parse with URL and compare protocol, origin, and required path rules.
Prevent it next time
Test relative, external, malformed, and disallowed-protocol values.

Client role checks protect a privileged action alone

Why it happens
An editable client is treated as trusted authority.
How to fix it
Enforce authorization and state rules on the server.
Prevent it next time
Include direct-request authorization tests in an isolated lab.

Check your understanding

1. Can a title containing <em> be shown safely without deleting characters?

Show worked solution

Yes, assign it to textContent on the intended element. Verify the literal text and absence of parsed child markup. If rich formatting is intentionally supported, that is a different contract requiring reviewed sanitization; do not switch to innerHTML merely because the title contains brackets.

2. A hidden administrator button is unreachable in the UI. What security evidence is still needed?

Show worked solution

In an authorized isolated test, send the corresponding action as a lower-privilege synthetic user and expect server denial with no sensitive side effect. Include an allowed administrator control case. UI absence establishes only presentation, not enforcement at the trusted boundary.

Key takeaways

  • Validation, safe output, and authorization address different trust questions.
  • Keep client-visible behavior separate from trusted security enforcement.

Official references