Skip to main content

Web Security Essentials

Security is a first-class concern, not an afterthought. This page is a condensed reference of the most common web security topics โ€” each section gives the core idea, what to do about it, and where to go deeper. For topic-specific deep dives, see the dedicated pages linked throughout this section.

XSS (Cross-Site Scripting)โ€‹

XSS occurs when an attacker injects malicious scripts into content that other users view. The browser executes the injected code in the context of the victim's session, giving the attacker access to cookies, tokens, and DOM manipulation. There are three main variants:

  • Stored XSS โ€” the payload is persisted (e.g., in a database) and served to every user who views the affected page.
  • Reflected XSS โ€” the payload is included in a request (e.g., a URL parameter) and reflected back in the response.
  • DOM-based XSS โ€” the payload never leaves the browser; client-side JavaScript reads attacker-controlled input and writes it into the DOM unsafely.

React auto-escapes JSX expressions by default, which covers most cases. The main React-specific risk is dangerouslySetInnerHTML โ€” if you must use it, always sanitize the input with DOMPurify first.

caution

Never pass unsanitized user input to dangerouslySetInnerHTML, eval(), or href="javascript:...". These are the most common XSS vectors in React applications.

Further reading: OWASP XSS Prevention Cheat Sheet

CSRF (Cross-Site Request Forgery)โ€‹

CSRF tricks a user's browser into making an unwanted request to a site where the user is already authenticated. The browser automatically attaches cookies, so the server can't distinguish the forged request from a legitimate one.

SameSite=Lax is the default for cookies in all modern browsers (since Chrome 80, February 2020). This blocks most CSRF attacks by default because the browser won't send the cookie on cross-origin POST requests. Token-based APIs that use Bearer tokens in the Authorization header are inherently immune โ€” no cookies means no CSRF. For cookie-authenticated forms that need additional protection, use anti-CSRF tokens or set SameSite=Strict.

Further reading: OWASP CSRF Prevention Cheat Sheet

CORS (Cross-Origin Resource Sharing)โ€‹

CORS is a mechanism that relaxes the browser's Same-Origin Policy. By default, browsers block frontend JavaScript from reading responses to cross-origin requests. The server opts in to cross-origin access by returning Access-Control-Allow-Origin headers specifying which origins are permitted.

A common misconfiguration is setting Access-Control-Allow-Origin: * alongside Access-Control-Allow-Credentials: true. Browsers reject this combination outright, but the intent signals a misunderstanding of the model โ€” wildcard and credentials are mutually exclusive. For non-simple requests (e.g., those with custom headers or PUT/DELETE methods), the browser sends a preflight OPTIONS request first to check whether the server allows the actual request.

tip

During development, CORS errors usually mean the server isn't configured yet, not that the client is doing something wrong. Resist the urge to install a "CORS-anywhere" proxy โ€” fix the server configuration instead.

Further reading: MDN CORS

CSP (Content Security Policy)โ€‹

CSP is an HTTP response header that restricts which sources the browser will load and execute scripts, styles, images, and other resources from. A well-configured CSP significantly reduces the impact of XSS โ€” even if an attacker finds an injection point, the browser refuses to execute scripts that aren't explicitly allowed.

Prefer a nonce-based approach over hash-based policies or domain allowlists. Nonces are unique per response, making them harder to bypass than static domain lists. Use Content-Security-Policy-Report-Only to test a policy in production without breaking anything โ€” violations are reported but not enforced, giving you time to tune the rules.

Further reading: Google CSP Evaluator ยท MDN CSP

HTTPS and TLSโ€‹

All traffic must be served over HTTPS. Use TLS 1.2 as the minimum supported version and prefer TLS 1.3 where available.

Set the Strict-Transport-Security (HSTS) header to tell browsers to always use HTTPS for your domain. Include a long max-age (at least two years) and consider adding includeSubDomains to cover all subdomains:

Strict-Transport-Security: max-age=63072000; includeSubDomains

In practice, use managed TLS termination โ€” cloud load balancers, Cloudflare, or Let's Encrypt. Don't terminate TLS in application code.

Security Headersโ€‹

These HTTP response headers provide defense-in-depth with minimal effort:

HeaderPurposeRecommended value
Strict-Transport-SecurityForce HTTPSmax-age=63072000; includeSubDomains
X-Content-Type-OptionsPrevent MIME sniffingnosniff
X-Frame-OptionsPrevent clickjackingDENY or SAMEORIGIN
Referrer-PolicyControl referrer leakagestrict-origin-when-cross-origin
Permissions-PolicyDisable unused browser featuresProject-specific
Content-Security-PolicyRestrict resource originsSee CSP section

For Node.js/Express applications, the helmet middleware sets all of these with sensible defaults in a single line of code.

tip

Run your deployed site through securityheaders.com for a quick audit.

Authentication and Sessionsโ€‹

Cookies used for authentication should always set these flags:

  • Secure โ€” only sent over HTTPS.
  • HttpOnly โ€” not accessible to JavaScript via document.cookie, which prevents XSS from stealing session tokens.
  • SameSite=Lax (or Strict) โ€” prevents the cookie from being sent on cross-origin requests, mitigating CSRF.

For JWT storage, prefer httpOnly cookies over localStorage. localStorage is accessible to any script running on the page โ€” a single XSS vulnerability exposes every token stored there. httpOnly cookies are invisible to JavaScript entirely.

For SPAs using OAuth 2.0, use the Authorization Code flow with PKCE. The Implicit flow is deprecated โ€” it exposes tokens in the URL fragment and provides no mechanism for refresh tokens.

Further reading: OWASP Authentication Cheat Sheet

Secrets Managementโ€‹

Never embed API keys, tokens, or credentials in frontend code. Browser bundles are public โ€” anyone can read them.

  • Use .env files locally and add .env to .gitignore.
  • Inject secrets at build time or runtime via CI/CD environment variables.
  • Keep truly secret values on the server side only โ€” proxy API calls through your backend if the frontend needs access to a protected resource.
danger

Any value bundled into a client-side JavaScript build is visible to anyone who opens DevTools. This includes values injected at build time via VITE_* or NEXT_PUBLIC_* environment variables. These prefixes mean "public" โ€” only use them for values that are truly non-secret.

Further Readingโ€‹