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.
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.
const label = document.createElement("span");
const externalTitle = "<em>sale</em>";
label.textContent = externalTitle;
console.log(label.textContent);
console.log(label.childElementCount);
<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.
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)"));
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.