Today’s goal
Treat the browser as a security runtime with explicit trust boundaries.
We will connect:
- origins, same-origin policy, and CORS;
- XSS, encoding, sanitization, CSP, and Trusted Types;
- CSRF, cookies, sessions, and logout;
- authentication, authorization, bearer tokens, OAuth, and PKCE;
- BFF architecture and token storage trade-offs;
- secrets, SRI, dependencies, and third-party scripts;
- clickjacking, framing, COOP, COEP, and CORP;
- secure messaging, iframes, logging, and a review checklist.
By the end of today you can
- identify the origin and trust boundary of a browser request;
- explain what CORS does and does not protect;
- trace untrusted input from source to dangerous sink;
- prefer safe text rendering and reviewed sanitization;
- use CSP as defense in depth;
- distinguish XSS from CSRF and authentication from authorization;
- choose cookie, BFF, or browser-token architecture deliberately;
- explain OAuth authorization code flow with PKCE;
- keep secrets out of browser bundles and URLs;
- review third-party, framing, messaging, and isolation risks.
Same-origin policy is not “no cross-origin activity”
Browsers may allow controlled cross-origin actions such as:
- loading images;
- submitting forms;
- embedding frames;
- sending requests;
- loading scripts under policy rules.
The key question is often whether the initiating page can read the response or control the embedded context.
CORS errors often reveal architecture problems
A CORS failure may indicate:
- API and app origins were not designed together;
- development proxy hid production behavior;
- credential policy is unclear;
- a public/private boundary is ambiguous;
- the browser is being asked to call a server that should be behind a BFF.
Fix the boundary, not only the console message.
XSS is more than <script> tags
Cross-site scripting occurs when attacker-controlled data becomes executable or dangerous browser content.
Possible paths include:
- HTML injection;
- event-handler attributes;
- dangerous URLs;
- script-capable SVG;
- template or expression injection;
- DOM APIs that interpret strings as markup.
Encoding and sanitization are different
output encoding → display a value as text in one context
sanitization → remove or constrain allowed markup and behaviorEncoding is context-specific.
Sanitization is a policy for permitting a restricted subset of content.
Neither should be applied blindly to every output context.
Avoid eval()-style execution
Never turn untrusted strings into code through:
eval();Function();- string-based timers;
- dynamic script construction;
- template expression interpreters without a trusted boundary.
If the product needs expressions, design a constrained language and parser rather than executing JavaScript.
Sanitizing rich HTML
If rich content is required:
- define the allowed elements and attributes;
- sanitize with a maintained, reviewed library;
- sanitize near the trust boundary;
- preserve the sanitized representation;
- render through one controlled component;
- test dangerous payloads and URL contexts.
Do not assume a generic “clean HTML” label communicates the policy.
Script policies matter most
Avoid broad script permissions where possible.
Prefer:
- external scripts from known origins;
- nonces or hashes for deliberate inline code;
- removal of inline handlers;
- restricted dynamic execution.
unsafe-inline and unsafe-eval should be treated as explicit trade-offs, not defaults.
CSP and third-party scripts
Third-party scripts expand the policy and trust surface.
For each script, ask:
- what data can it read?
- what can it send?
- what happens if it changes?
- can it be removed or isolated?
- is its origin and integrity controlled?
Allowing a script is granting code execution in the page’s origin.
Operational capabilities of a BFF
A Backend-for-Frontend can:
- Hold server credentials: keep sensitive API keys and tokens out of browser memory;
- Manage sessions: issue encrypted
HttpOnly,SameSite=Strictcookies to the SPA; - Aggregate responses: combine multiple downstream microservice calls into one tailored payload;
- Perform edge transformations: translate internal protocols without client complexity.
It replaces direct client token management with a hardened same-origin boundary.
Browser-based OAuth has its own threat model
Consider:
- redirect interception;
- authorization-code injection;
- open redirects;
- state and nonce validation;
- browser history and referrer leakage;
- token exposure to scripts;
- malicious extensions or compromised dependencies.
The browser is not a confidential client environment.
Refresh tokens need stronger protection
Refresh tokens can create long-lived access.
Consider:
- whether the browser should receive them;
- rotation and reuse detection;
- secure cookie or BFF storage;
- revocation;
- device and session binding;
- logout behavior.
Do not treat refresh credentials like ordinary UI state.
Reduce supply-chain exposure
Use:
- minimal dependencies;
- lockfiles and review;
- trusted registries;
- vulnerability and behavior monitoring;
- restricted scripts where appropriate;
- separate build and deploy credentials;
- reproducible artifacts.
Pinning helps reproducibility but does not eliminate malicious or compromised code.
Security headers need testing
Test headers in:
- production-like origins;
- authenticated and unauthenticated flows;
- embedded and popup scenarios;
- worker and asset loading;
- third-party integrations;
- error and redirect paths.
A header that “looks secure” but breaks recovery or silently disables a feature is not a finished design.
Framework escaping is good - but not sufficient
React, Vue, and other frameworks make common text rendering safer by default.
They cannot decide:
- whether a URL is allowed;
- whether rich HTML should be sanitized;
- whether a third-party script is trustworthy;
- whether an API call is authorized;
- whether a token belongs in the browser.
Framework safety is one layer in a larger boundary model.
Security review checklist: authentication and authorization
Verify:
- identity is established through a supported flow;
- tokens are validated by the correct server;
- permissions are enforced server-side;
- client UI reflects but does not enforce authority;
- logout clears or revokes relevant state;
- redirects and callbacks are constrained.
Practical lab: Secure a Front-End Application Boundary
Review and harden a small application boundary involving untrusted input, cross-origin requests, cookies, OAuth-style redirects, and dangerous sinks.
The practical turns each security concept into an observable browser behavior and documented design decision.
Practical stages 5–10: rendering defenses
- Demonstrate safe text rendering.
- Identify a dangerous sink.
- Add rich-text sanitization.
- Add CSP in report-only mode.
- Enforce a basic CSP.
- Explore Trusted Types.
Document which content is text, which is approved rich HTML, and which sinks remain intentionally available.
Troubleshooting guide (Part 1)
| Symptom | Likely cause |
|---|---|
| CORS fails in production only | Development proxy hid real origins |
| Browser blocks a request but curl succeeds | CORS governs browser reads, not server access |
| Escaped text still creates a dangerous link | URL context was not validated |
| CSP breaks analytics or workers | Dependencies were not inventoried before enforcement |
| CSRF token is missing on a mutation | Cookie session and request protection were not designed together |
Troubleshooting guide (Part 2)
| Symptom | Likely cause |
|---|---|
| Hidden button is treated as authorization | Server enforcement is missing |
| Token payload looks valid | Decoding is not signature or audience validation |
| Logout leaves private data visible | Client caches, connections, or local storage were not cleared |
| iframe integration breaks after isolation headers | Policy was copied without testing dependencies |
Completion checklist
- origins and cross-origin relationships are documented;
- CORS is treated as browser read permission, not server security;
- untrusted inputs and dangerous sinks are mapped;
- text rendering is preferred over HTML;
- rich HTML has a reviewed sanitization policy;
- CSP and Trusted Types are defense in depth;
- cookie mutations have CSRF protection decisions;
- authentication and authorization are separate;
- tokens and secrets stay within appropriate boundaries;
- third-party and browser-isolation policies are tested.
Misconceptions to leave behind (Part 1)
| Misconception | Better mental model |
|---|---|
| CORS protects the API from attackers | It controls browser script read access |
| CORS failure means the server did not receive the request | Browser policy and server receipt differ |
XSS means only <script> tags | Any attacker-controlled executable context can matter |
| Framework escaping makes XSS impossible | Escape hatches and context-specific sinks remain |
| Sanitization and encoding are the same | They solve different content problems |
| CSP prevents XSS by itself | It is defense in depth |
| CSRF and XSS are the same | They exploit different trust paths |
Misconceptions to leave behind (Part 2)
| Misconception | Better mental model |
|---|---|
| HttpOnly prevents XSS | It limits direct cookie reads, not same-origin actions |
| Hidden controls enforce permission | Server authorization must enforce it |
| JWT means secure authentication | JWT is a format, not an architecture |
| ID and access tokens are interchangeable | They serve different audiences and purposes |
.env values are secret | Client-exposed build values are public |
| Lockfiles eliminate supply-chain risk | They improve reproducibility, not trust |
| COOP, COEP, and CORP are interchangeable | Each controls a different browser boundary |