Tutorial: Harden a Frontend App for Security and Reliability
Why this tutorial matters
Security and reliability are not separate late-stage checklists. In the browser, they share boundaries: scripts, sessions, storage, network calls, third-party integrations, generated content, error handling, and degraded states.
This tutorial hardens a mid-level Account Portal with authentication, profile editing, document upload, notifications, and an AI assistant side panel. The app is intentionally realistic enough to force meaningful decisions.
What you will build
| Capability | Purpose |
|---|---|
| CSP and security headers | Reduce XSS and clickjacking risk |
| Trusted rendering path | Prevent unsafe HTML/generated content |
| Auth/session boundaries | Avoid token leakage and confused session state |
| Error boundaries | Contain UI failures |
| Degraded modes | Keep core tasks available during partial outages |
| Accessibility gates | Reliability for keyboard/screen-reader users |
| Chaos drills | Test API failure, slow network, and third-party outage |
| Incident playbooks | Respond consistently |
Milestone 1: Threat model the frontend
Create a threat table:
| Asset | Threat | Control |
|---|---|---|
| session | token theft via XSS | httpOnly cookies, CSP, no token in localStorage |
| profile form | CSRF or invalid mutation | CSRF protection, same-site cookies, server validation |
| document preview | malicious file/content | sandboxed preview, content-type checks |
| AI assistant | prompt/content injection | schema validation, no arbitrary HTML |
| third-party scripts | supply-chain script risk | script inventory, CSP, owner approval |
Acceptance criteria:
- top five threats have controls
- risky assumptions are documented
- controls map to tests or review checks
Milestone 2: Add security headers
Implement headers:
Content-Security-Policy
Strict-Transport-Security
X-Content-Type-Options
Referrer-Policy
Permissions-Policy
frame-ancestors via CSP
Start CSP in report-only mode:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' 'nonce-{nonce}';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
report-uri /api/csp-report;
Then tighten after reports are understood.
Acceptance criteria:
- CSP reports are collected and reviewed.
- Inline scripts require nonce/hash or are removed.
- App cannot be framed where not intended.
- HSTS is enabled for production HTTPS.
Milestone 3: Safe rendering and generated content
Create one rich-text/generated-content path and harden it.
Rules:
- markdown is parsed through a controlled renderer
- raw HTML is disabled or sanitized with strict allowlist
- links are validated
- generated UI uses schema-validated components
- no model output becomes executable code
Acceptance criteria:
- malicious fixtures do not execute script
- unsafe URLs are rejected
- generated UI fallback handles invalid payloads
- CSP catches unexpected script execution
Milestone 4: Auth/session architecture
Session rules:
- prefer httpOnly secure same-site cookies for web sessions
- never store long-lived access tokens in localStorage
- server validates authorization on every sensitive action
- frontend treats auth state as a hint, not authority
- logout clears client caches and sensitive view state
Test cases:
| Case | Expected behavior |
|---|---|
| session expires during edit | preserve draft, prompt re-auth |
| user loses permission | hide/disable action after server denial |
| logout in another tab | sensitive cached data is cleared |
| back button after logout | no private data reappears |
Milestone 5: Reliability boundaries
Add error boundaries by route and widget:
AppBoundary
RouteBoundary
AccountSummary
DocumentListBoundary
AssistantBoundary
Rules:
- assistant failure does not break account editing
- document preview failure does not break document list
- route-level error has recovery action
- client errors include release, route, component, and correlation id
Acceptance criteria:
- throwing in assistant side panel keeps primary form usable
- failed document API shows local degraded state
- error telemetry identifies component owner
Milestone 6: Degraded modes
Define degradation:
| Dependency failure | Degraded behavior |
|---|---|
| recommendations API down | hide recommendations, keep core account actions |
| document preview down | allow download or retry |
| AI assistant down | show deterministic help/search |
| analytics down | queue/drop safely, never block user |
| upload slow | show progress, cancellation, retry |
Acceptance criteria:
- each dependency has user-facing fallback
- noncritical dependencies do not block primary task
- retry loops are bounded
Milestone 7: Accessibility as reliability
WCAG 2.2 principles map to reliable operation: perceivable, operable, understandable, robust.
Hardening checks:
- keyboard access for every action
- focus management on route changes/dialogs/errors
- error messages associated with fields
- live regions for async status
- sufficient contrast in normal and error states
- reduced motion respected
Acceptance criteria:
- account edit can be completed with keyboard
- screen reader announces validation and save status
- automated checks run, but manual keyboard testing is required
Milestone 8: Chaos drills
Run drills:
| Drill | Expected result |
|---|---|
| API returns 500 | route stays usable where possible |
| API latency 10s | timeout and retry UI appear |
| upload fails at 80% | user can retry/cancel |
| CSP violation | report is captured |
| third-party script blocked | app core still works |
| assistant emits invalid UI | fallback renders |
Release gate
Do not release until:
- CSP report-only is clean enough to enforce core policy
- high-risk mutations have server validation and CSRF protection
- generated/rich content path is tested against malicious fixtures
- route error boundaries are in place
- key degraded modes are tested
- accessibility smoke tests pass
- incident playbook exists
Security test fixtures
Create malicious fixtures:
<img src=x onerror=alert(1)>
<a href="javascript:alert(1)">click</a>
<iframe src="https://attacker.example"></iframe>
Generated UI fixture:
{
"type": "approval_action",
"props": {
"label": "Send all customer records",
"url": "javascript:alert(1)"
}
}
Expected behavior:
- raw HTML is not executed
- unsafe link is removed or rejected
- iframe is blocked unless explicitly allowed
- generated action fails validation
- CSP report is emitted for unexpected script execution
Reliability scorecard
Track:
| Metric | Why |
|---|---|
| route error rate | detects broken releases |
| API failure rate by dependency | identifies partial outage |
| retry rate | detects flaky network/service behavior |
| abandoned form rate after error | detects user harm |
| CSP violations | detects security drift |
| accessibility known defects | tracks reliability for assistive tech |
| degraded-mode usage | shows whether fallback paths work |
Scorecards should have owners. A metric without owner becomes background noise.
Incident drill script
Run a 30-minute drill:
- Break the document preview API.
- Confirm route still loads.
- Confirm user can continue core account task.
- Confirm telemetry includes dependency and route.
- Confirm support-facing message is accurate.
- Restore service.
- Write a short postmortem and one prevention action.
Repeat with AI assistant invalid UI and expired session during form edit.
Policy decisions to document
Write ADRs for:
- session storage model
- CSP rollout strategy
- rich-text/generated-content rendering path
- third-party script approval process
- degraded-mode ownership
- accessibility release gate
These decisions are security architecture. They should not live only in code comments.
Source lens
This tutorial is informed by MDN CSP guidance, web.dev security headers guidance, WCAG 2.2 principles, and the book's chapters on security architecture, browser attack surface, auth/session architecture, resilience patterns, frontend SRE, chaos gamedays, and accessibility.