Security for Frontend Engineers
(Reducing exploitability in the browser)
Senior rule:
The frontend is not the authority, but it is still part of the attack surface.
Atlas contains account data, revenue reports, roles, saved views, and analytics scripts. A senior frontend engineer must understand how browser code can expose data or widen blast radius.
Conceptual Model
Frontend security focuses on:
- untrusted input
- unsafe rendering
- session exposure
- cross-site requests
- third-party scripts
- dependency and supply-chain risk
- stale permission assumptions
Server authorization is the source of truth. Frontend security reduces exposure and preserves trust.
XSS
XSS happens when untrusted data becomes executable script or trusted markup.
Dangerous pattern:
export function UnsafeComment({ html }: { html: string }) {
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
Safer default:
export function Comment({ text }: { text: string }) {
return <p>{text}</p>;
}
If HTML is required, sanitize it with a reviewed library, restrict allowed tags, and pair it with CSP.
Watch for:
- Markdown rendering
- rich text editors
- third-party embeds
- user-controlled URLs
- HTML strings in translations
- dangerous DOM APIs
Content Security Policy
CSP limits what the browser is allowed to execute or load.
export const securityHeaders = [
{
key: "Content-Security-Policy",
value: [
"default-src 'self'",
"script-src 'self' https://analytics.example.com",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"connect-src 'self' https://api.example.com",
"frame-ancestors 'none'",
"base-uri 'self'",
"form-action 'self'",
].join("; "),
},
];
Treat CSP as architecture: it defines allowed runtime behavior. Start with reporting if the app is not ready for enforcement.
SRI for External Resources
Subresource Integrity lets the browser verify that an external static resource matches an expected hash.
<script
src="https://cdn.example.com/chart-lib.1.2.3.js"
integrity="sha384-BASE64_HASH"
crossorigin="anonymous"
></script>
Use SRI for versioned CDN scripts and styles where feasible. Do not use mutable CDN URLs for critical code.
CSRF and Cookies
CSRF matters when browsers automatically attach credentials to state-changing requests.
Senior frontend engineers should understand:
HttpOnlyprotects cookies from JavaScript accessSecurerequires HTTPSSameSite=LaxorSameSite=Strictreduces cross-site sending- anti-CSRF tokens may still be needed depending on flow
- GET should not mutate state
type SessionCookiePolicy = {
httpOnly: true;
secure: true;
sameSite: "lax" | "strict";
path: "/";
};
AuthN, AuthZ, and RBAC
The UI can hide controls, but the server must enforce permissions.
type Permission = "reports:view" | "reports:export" | "users:invite";
export function ExportButton({
permissions,
}: {
permissions: Permission[];
}) {
if (!permissions.includes("reports:export")) return null;
return <button>Export report</button>;
}
This improves UX, not security. The export endpoint must still authorize the request.
Design for:
- permission refresh
- stale role changes
- SSO callback and session bootstrap states
- session expiry
- logout across tabs
- no tokens in local logs, URLs, or analytics
SSO and Session Bootstrap
Single Sign-On often creates frontend edge cases:
- callback routes may briefly have no complete session
- role and profile data may arrive after identity is known
- users may return to a deep link after login
- session refresh can fail while a form is dirty
- logout needs to clear local UI state across tabs
type SessionState =
| { status: "checking" }
| { status: "authenticated"; userId: string; roles: string[] }
| { status: "unauthenticated"; returnTo: string };
Do not treat SSO as a button. Treat it as a state machine around identity, routing, permissions, and recovery.
Third-Party Scripts
Every third-party script can affect:
- execution
- privacy
- performance
- data exposure
- CSP policy
type ScriptThreatReview = {
vendor: string;
dataAccessed: string[];
routes: string[];
cspSource: string;
consentRequired: boolean;
owner: string;
removalPlan: string;
};
If nobody owns the script, the architecture owns the risk badly.
Mini Case Study: Export Button Leak
Atlas hid the export button for viewers, but the API accepted the request if a user knew the URL.
Fix:
- enforce authorization server-side
- refresh permissions after role changes
- add UI tests for hidden controls
- add API contract tests for forbidden export
- log denied access attempts
The frontend mistake was assuming visibility equals authority.
Common Failure Modes
- Trusting client-side RBAC.
- Treating SSO callback and permission loading as a single boolean.
- Storing tokens in
localStoragewithout threat review. - Rendering unsanitized HTML.
- Adding scripts that force weak CSP.
- Logging PII or tokens.
- Using mutable third-party URLs.
- Treating CORS as authorization.
Review Checklist
- Is untrusted content rendered safely?
- Is CSP documented and tested?
- Are external static assets protected with SRI where feasible?
- Are cookies configured with secure attributes?
- Are state-changing requests protected from CSRF?
- Is authorization enforced server-side?
- Are SSO callback, deep-link, and session-refresh states designed?
- Are tokens excluded from URLs, logs, and analytics?
- Are third-party scripts reviewed?
Senior Deep Dive: Browser Trust Boundaries
Senior frontend security is not memorizing attack names. It is knowing where trust changes inside the browser and making unsafe paths hard to create.
Frontend trust-boundary map
| Boundary | Risk | Senior control |
|---|---|---|
| server data to DOM | XSS through unsafe rendering | safe renderers, sanitizer, Trusted Types, CSP |
| URL to app state | open redirect, injection, confused filters | typed parsing, allowlists, canonicalization |
| browser storage to UI | stale auth or leaked sensitive data | storage rules, expiry, logout cleanup |
| third-party script to page | data exfiltration, performance, mutation | ownership, consent, CSP, isolation, kill switch |
| iframe/postMessage to app | spoofed commands or token leakage | origin checks, schema validation, minimal messages |
| UI permission to server action | hidden controls mistaken for auth | server authorization and negative tests |
The weakness this corrects: mid-level engineers often think "the backend handles security." The backend is authoritative, but frontend code can still leak data, widen exploitability, and mislead users.
Secure rendering contract
Any feature that renders rich content needs a written contract:
- source of content: user, admin, CMS, translation, partner, internal system
- allowed content model: plain text, Markdown subset, sanitized HTML, trusted system markup
- sanitizer owner and configuration
- allowed URL schemes
- image and iframe policy
- test fixtures for malicious content
- CSP and Trusted Types compatibility
- review requirement for new allowed tags or attributes
Do not let each component decide whether a string is safe. Safety should be centralized, typed, and tested.
Session and permission drift
Frontend apps often keep session snapshots, role lists, feature flags, tenant context, and cached profile data. These can drift from server truth. Senior engineers design for drift: refetch permissions after tenant switch or privilege-sensitive navigation, handle 401/403 as normal states, clear cached privileged data on logout and account switch, avoid showing irreversible actions based only on stale client state, and treat server denial as authoritative even when UI thought action was allowed.
Third-party and dependency risk review
Before adding a browser dependency or script, answer:
| Question | Why |
|---|---|
| Does it execute in the user's browser? | browser execution can access page context |
| Does it parse untrusted input? | parsers often create security risk |
| Does it add transitive dependencies? | supply-chain and bundle exposure |
| Does it phone home? | privacy, compliance, and debugging |
| Can it be isolated or lazy-loaded? | reduces blast radius |
| Who updates it after advisory? | prevents orphaned risk |
This is senior behavior because it weighs security, performance, privacy, and maintainability together.
Security test placement
Security checks should exist at multiple layers: unit tests for URL parsing and sanitizer policy, integration tests for 401/403 and stale-session behavior, E2E tests for logout and protected actions, header tests for CSP and cookies, dependency scanning with owner review, and production reporting for CSP violations and auth errors. Security that exists only as a checklist will drift.
Exercises
Exercise 1 - XSS Sink Audit
Search the codebase for dangerous sinks and document why each is safe or unsafe.
Exercise 2 - Permission Drift Drill
Simulate a user losing a permission while the app is open. What does the UI do?
Exercise 3 - Script Threat Review
Pick one third-party script and complete a script threat review record.
Further Reading
- OWASP: XSS Prevention Cheat Sheet - https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html
- OWASP: Content Security Policy Cheat Sheet - https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html
- OWASP: CSRF Prevention Cheat Sheet - https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html
- MDN: Content Security Policy - https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
- MDN: Subresource Integrity - https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Subresource_Integrity