Chapter 31 · Security Best Practices
Subchapter 31.6
references/javascript-typescript-react-web-frontend-security.mdMarkdown41 KBView on GitHub
This document is designed as a security spec that supports:
Also bundled
OpenAIIt is intentionally written as a set of normative requirements (“MUST/SHOULD/MAY”) plus audit rules (what bad patterns look like, how to detect them, and how to fix/mitigate them).
MUST NOT request, output, log, or commit secrets (API keys, OAuth client secrets, private keys, session cookies, JWTs, signing keys).
MUST NOT “fix” security by disabling protections (e.g., turning off CSP to “make it work”, adding unsafe-inline/unsafe-eval without a documented, constrained plan, disabling CSRF protections when using cookies, widening CORS, skipping sanitization, or “temporary” bypasses that ship). (OWASP Cheat Sheet Series (opens in a new tab))
MUST provide evidence-based findings during audits: cite file paths, code snippets, and configuration values that justify the claim.
MUST treat uncertainty honestly: if a protection might exist in infra (CDN/WAF/reverse proxy), report it as “not visible in app code; verify via runtime headers / edge config”.
MUST assume any data that crosses a trust boundary (URL, storage, network, postMessage, third-party scripts) can be attacker-influenced unless proven otherwise (see §2.1).
When asked to write new React code or modify existing code:
innerHTML, dynamic code execution, untrusted redirects/navigation, third‑party script injection, unsafe token storage, etc.). (MDN Web Docs (opens in a new tab))While working anywhere in a React repo (even if the user did not ask for a security scan):
When the user asks to “scan”, “audit”, or “hunt for vulns”:
Recommended audit order:
dangerouslySetInnerHTML, markdown/HTML renderers, URL attributes.innerHTML, eval, new Function, document.write, etc.).window.location, target=_blank, window.open).Examples include:
window.location, query params, hash fragments, route params.localStorage, sessionStorage, IndexedDB (including data previously written by the app—because XSS or extensions can tamper with it). (OWASP Cheat Sheet Series (opens in a new tab))window.postMessage payloads. (OWASP Cheat Sheet Series (opens in a new tab))A request is state-changing if it can create/update/delete data, change auth/session state, trigger side effects (purchase, email send, webhook), or initiate privileged actions.
Frontend-specific note:
fetch/axios calls or form submissions. If authentication is cookie-based, these calls can be CSRF-relevant (§4 REACT-CSRF-001). (OWASP Cheat Sheet Series (opens in a new tab))For each issue found, output:
This is the smallest “production baseline” that prevents common React frontend misconfigurations.
MUST ship a production build (minified, no dev-only overlays/tools, correct mode flags).
MUST ensure build-time configuration does not embed secrets into the shipped JS/HTML/CSS. Build-time “environment variables” are not secret; treat them as public. (create-react-app.dev (opens in a new tab))
SHOULD treat source maps as sensitive operational artifacts:
unsafe-inline and unsafe-eval unless strictly necessary and documented). (OWASP Cheat Sheet Series (opens in a new tab))frame-ancestors (CSP) and/or X-Frame-Options, unless embedding is an explicit product requirement. (MDN Web Docs (opens in a new tab))If rendering any user-provided HTML/markdown/rich text:
If using service workers / PWA:
Each rule contains: required practice, insecure patterns, detection hints, and remediation.
Severity: Critical (if secrets exposed)
Required:
public/ assets, or in build-time environment variables intended for client consumption.Insecure patterns:
Using build-time env vars for secrets:
process.env.REACT_APP_* containing private keys or credentials.import.meta.env.VITE_* containing secrets.Hard-coded secrets in JS/TS, .env committed, or secrets in public/config.json served to all users.
Detection hints:
Search for:
REACT_APP_, VITE_, NEXT_PUBLIC_, process.env., import.meta.env.apiKey, secret, token, private, password, client_secretInspect public/ for runtime config JSON.
Fix:
Notes:
Severity: High (Only if you can prove attacker-controlled HTML reaches it)
Required:
MUST avoid dangerouslySetInnerHTML unless absolutely necessary.
If it must be used:
Insecure patterns:
<div dangerouslySetInnerHTML={{ __html: userHtml }} /> where userHtml is from API/URL/storage.Detection hints:
dangerouslySetInnerHTML, __html:Fix:
Replace with safe rendering:
Add CSP; remove dangerous sinks where possible.
Notes:
dangerouslySetInnerHTML is dangerous and can introduce XSS if misused. (React (opens in a new tab))dangerouslySetInnerHTML without sanitization as a common framework “escape hatch” pitfall. (OWASP Cheat Sheet Series (opens in a new tab))Severity: High (when bypassed)
Required:
{value}) and React props, which are escaped by default.Insecure patterns:
Converting untrusted text into HTML and injecting it:
element.innerHTML = userValuedocument.write(userValue)insertAdjacentHTML(..., userValue)Detection hints:
innerHTML, outerHTML, insertAdjacentHTML, document.write, DOMParser, createContextualFragment.Fix:
Notes:
Severity: High
Required:
MUST avoid direct DOM injection sinks, even outside React rendering, unless strongly controlled.
If a DOM sink is required:
Insecure patterns:
someEl.innerHTML = untrusteddocument.write(untrusted)new DOMParser().parseFromString(untrusted, 'text/html') followed by insertionDetection hints:
innerHTML, outerHTML, document.write, DOMParser, Range().createContextualFragment, insertAdjacentHTMLFix:
Prefer:
textContent for text insertion.Notes:
Element.innerHTML and document.write() as injection sinks that can execute script when given attacker-controlled input. (MDN Web Docs (opens in a new tab))textContent instead of innerHTML for assigning untrusted data. (OWASP Cheat Sheet Series (opens in a new tab))Severity: High Only when you can prove they are attacker controlled
Required:
MUST treat any URL derived from untrusted input as dangerous.
MUST allowlist schemes and (when applicable) hosts:
https: (and maybe http: for localhost/dev) and relative URLs for in-app navigation.javascript: and dangerous data: uses unless you have specialized validation and a clear use case.SHOULD prefer same-site relative paths (e.g., /settings) over absolute URLs.
MUST validate “returnTo/next/redirect” parameters (see REACT-REDIRECT-001).
Insecure patterns:
<img src={userProvidedUrl}>... (can be used for tracking / data exfil; also risky if used for scripts/iframes)window.location = nextnavigate(next) where next comes from query params without validationDetection hints:
Search for:
href={, src={, window.location, location.href, window.open, navigate(, redirectTo, returnTo, next=Track whether the value is derived from URL/query/storage/API.
Fix:
Implement a shared safeUrl() utility:
new URL(value, base)/) or a strict allowlist of absolute origins.Fall back to a safe default when validation fails.
Notes:
dangerouslySetInnerHTML risk and also states React cannot safely handle javascript: or data: URLs without specialized validation. (OWASP Cheat Sheet Series (opens in a new tab))Severity: Medium
Required:
MUST assume markdown/rich text can be attacker-controlled if it comes from users or CMS.
MUST ensure raw HTML is not rendered unless sanitized.
SHOULD prefer markdown renderers that:
Insecure patterns:
Detection hints:
Search for common libraries and risky options:
marked, markdown-it, react-markdown, rehype-raw, sanitize: false, allowDangerousHtml, etc.Look for dangerouslySetInnerHTML used with “markdown output”.
Fix:
Notes:
Severity: Low
Required:
Insecure patterns:
Detection hints:
Search for:
trustedTypes.createPolicyrequire-trusted-types-for, trusted-typesSearch for remaining DOM sinks (REACT-DOM-001).
Fix:
Implement a small number of tightly scoped policies:
Run in report-only mode, fix violations, then enforce.
Notes:
innerHTML, document.write) and JS URL sinks (script.src). (MDN Web Docs (opens in a new tab))Severity: Medium to High
Required:
unsafe-inline and unsafe-eval when possible.Insecure patterns:
unsafe-inline/unsafe-eval broadly without justification.script-src * or overly broad sources.Detection hints:
Look for CSP configuration:
index.html responses, or framework config.If absent in repo, mark as “verify at edge”.
Fix:
Notes:
Severity: Low
Required:
MUST treat third-party JS as equivalent to running arbitrary code in your origin.
If loading from a CDN or third party:
integrity=...) and crossorigin where applicable.Insecure patterns:
<script src="https://cdn.example.com/lib/latest.js"></script> with no integrity.Detection hints:
Search in public/index.html, templates, or SSR wrappers for:
<script src=, <link rel="stylesheet" href=Identify scripts loaded dynamically in runtime JS.
Fix:
Notes:
Severity: High
Required:
MUST minimize third-party scripts and treat each as a supply-chain risk.
MUST know exactly what third-party JS executes in your origin and why.
SHOULD implement governance:
Insecure patterns:
Detection hints:
Search for common vendor snippets in HTML/JS:
Look for dynamic script insertion:
document.createElement('script'), .src = ..., .appendChild(script)Fix:
Reduce to only necessary vendors.
Where feasible:
Notes:
Severity: Medium
Required:
SHOULD avoid storing session identifiers or long-lived tokens in localStorage (and generally in Web Storage) because XSS can exfiltrate them.
If tokens must exist client-side:
SHOULD prefer HTTPOnly cookies for session tokens when possible (requires CSRF strategy: see REACT-CSRF-001).
Insecure patterns:
localStorage.setItem('token', ...) / sessionStorage.setItem('token', ...) for auth tokens.localStorage.Detection hints:
localStorage., sessionStorage., setItem(, getItem(, token, jwt, refreshFix:
Notes:
Severity: High
NOTE: If the application does not use cookie based auth (using Authentication header for example), then CSRF is not a concern.
Required:
If the app relies on cookies for authentication:
Insecure patterns:
fetch('/api/transfer', { method: 'POST', credentials: 'include' }) with no CSRF token/header, relying only on cookies.Detection hints:
Enumerate state-changing network calls and check:
credentials: 'include' or withCredentials: true used?X-CSRF-Token)?Search for “csrf” utilities; if absent, treat as suspicious.
Fix:
Add CSRF token flow:
Keep SameSite cookies and Origin/Referer validation as defense-in-depth.
Notes:
Severity: High (only if used as primary protection)
Required:
Insecure patterns:
if (user.isAdmin) { showAdminPanel(); } with no server-side enforcement.Detection hints:
Fix:
Notes:
Severity: Medium to High
Required:
MUST avoid making authenticated requests to attacker-controlled origins.
SHOULD avoid allowing user input to control request destination (scheme/host/port).
SHOULD centralize network clients (fetch/axios) with:
baseURL (or strict allowlist),credentials usage.Insecure patterns:
fetch(userProvidedUrl, { credentials: 'include' })axios.create({ baseURL: userProvidedBase })Detection hints:
Search for fetch( / axios( where the first argument or baseURL is derived from:
Search for credentials: 'include', withCredentials: true.
Fix:
Notes:
Severity: Medium
Required:
next, returnTo, redirect).Insecure patterns:
window.location.href = new URLSearchParams(location.search).get('next')navigate(next) where next comes from query params.Detection hints:
next, returnTo, redirect, window.location, navigate(Fix:
/^\/[^\s]*$/) or allowlisted origins./) when invalid.Notes:
Severity: Medium
Required:
localhost dev), and deploy only in secure contexts.Insecure patterns:
Detection hints:
Search for:
navigator.serviceWorker.registerworkbox, precacheAndRoute, custom fetch handlersInspect caching patterns (caches.open, cache.put, respondWith).
Fix:
Notes:
Severity: Medium
Required (typical SPA served from an origin):
SHOULD set:
Content-Security-Policy)X-Content-Type-Options: nosniffframe-ancestors in CSP and/or X-Frame-Options)Referrer-PolicyPermissions-Policy as appropriateMUST ensure these are set somewhere (CDN/edge/server), even if not in repo.
Insecure patterns:
Detection hints:
Fix:
Notes:
X-Frame-Options and CSP frame-ancestors. (MDN Web Docs (opens in a new tab))Severity: Medium to High (depends on what messages can do)
Required:
targetOrigin when sending messages (not *) unless there is a strict reason.event.origin on receipt and validate message shape.Insecure patterns:
window.postMessage(data, '*') to unknown targets.
Receiving:
window.addEventListener('message', (e) => { eval(e.data) })element.innerHTML = e.dataDetection hints:
postMessage(, addEventListener('message'Fix:
Notes:
postMessage, checking sender origin, validating data, and avoiding eval/innerHTML with message content. (OWASP Cheat Sheet Series (opens in a new tab))Severity: Medium (can be High if stored-XSS possible)
Required:
Insecure patterns:
dangerouslySetInnerHTML or <iframe srcdoc=...> without sanitization.Detection hints:
Search for upload components and preview logic:
input type="file", FileReader, URL.createObjectURL, <iframe>, <object>, <embed>.Trace where uploaded content is later displayed.
Fix:
Notes:
Severity: Low
Required:
MUST use a lockfile and enforce reproducible installs in CI.
SHOULD regularly audit dependencies and respond quickly to advisories for:
SHOULD reduce exposure to install-time script attacks and typosquatting risk.
Audit focus:
npm ci (or Yarn frozen lockfile / pnpm equivalent) to prevent drift.npm audit, GitHub Dependabot/alerts, etc.).Insecure patterns:
npm install in CI producing non-reproducible builds.Detection hints:
package-lock.json, yarn.lock, pnpm-lock.yaml.npm install vs npm ci.postinstall scripts and suspicious build steps.Fix:
npm ci).Notes:
npm audit as submitting the project dependency tree to the registry to receive a report of known vulnerabilities and (optionally) applying remediations via npm audit fix, while noting some vulns require manual review. (npm Docs (opens in a new tab))npm ci as intended for automated/CI environments, requiring an existing lockfile and failing if package.json and lockfile do not match. (npm Docs (opens in a new tab))npm ci / yarn install --frozen-lockfile to abort on inconsistencies, and highlights the risk of install-time scripts and the option to use --ignore-scripts to reduce attack surface. (OWASP Cheat Sheet Series (opens in a new tab))When actively scanning, use these high-signal patterns:
Raw HTML / XSS escape hatches:
dangerouslySetInnerHTML, __html:rehype-raw, allowDangerousHtml, sanitize: falseDOM XSS sinks:
innerHTML, outerHTML, insertAdjacentHTML, document.write, DOMParser, createContextualFragmentDangerous JS execution:
eval(, new Function(, setTimeout(", setInterval("Untrusted URL injection / navigation:
href={ / src={ with untrusted valueswindow.location, location.href, window.open, navigate(next, returnTo, redirectToken/session risk:
localStorage.setItem, sessionStorage.setItem, getItem( with token, jwt, refreshCookie/CSRF coupling:
credentials: 'include', withCredentials: true on state-changing requests without CSRF headersThird-party scripts:
<script src=...> in public/index.htmlService workers:
navigator.serviceWorker.register, Workbox usage, custom fetch handlerspostMessage:
postMessage( with *, missing event.origin checksSupply chain:
npm install, no audit step, risky postinstall scriptsAlways try to confirm:
Primary React documentation:
https://react.dev/blog/2024/12/05/react-19 (React (opens in a new tab))dangerouslySetInnerHTML warning — https://react.dev/reference/react-dom/components/common#dangerouslysetting-the-inner-html (React (opens in a new tab))https://legacy.reactjs.org/docs/introducing-jsx.html (React (opens in a new tab))OWASP Cheat Sheet Series:
dangerouslySetInnerHTML; URL validation notes) — https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html (OWASP Cheat Sheet Series (opens in a new tab))https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html (OWASP Cheat Sheet Series (opens in a new tab))https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html (OWASP Cheat Sheet Series (opens in a new tab))https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html (OWASP Cheat Sheet Series (opens in a new tab))https://cheatsheetseries.owasp.org/cheatsheets/Third_Party_Javascript_Management_Cheat_Sheet.html (OWASP Cheat Sheet Series (opens in a new tab))https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html (OWASP Cheat Sheet Series (opens in a new tab))https://cheatsheetseries.owasp.org/cheatsheets/NPM_Security_Cheat_Sheet.html (OWASP Cheat Sheet Series (opens in a new tab))Browser / platform references (MDN, W3C):
https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API (MDN Web Docs (opens in a new tab))https://www.w3.org/TR/trusted-types/ (W3C (opens in a new tab))https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity (MDN Web Docs (opens in a new tab))https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Clickjacking (MDN Web Docs (opens in a new tab))https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers (MDN Web Docs (opens in a new tab))https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Secure_Contexts (MDN Web Docs (opens in a new tab))rel values (noopener/noreferrer) — https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel (MDN Web Docs (opens in a new tab))Build tooling / env exposure references:
https://create-react-app.dev/docs/adding-custom-environment-variables/ (create-react-app.dev (opens in a new tab))https://vite.dev/guide/env-and-mode (vitejs (opens in a new tab))Auth/token storage guidance:
https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps ([IETF Datatracker][16])Dependency tooling references:
https://docs.npmjs.com/cli/v10/commands/npm-audit/ (npm Docs (opens in a new tab))https://docs.npmjs.com/cli/v10/commands/npm-ci/ (npm Docs (opens in a new tab))Sanitizer reference:
https://github.com/cure53/DOMPurify (GitHub (opens in a new tab))[16]: https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps (opens in a new tab) “
draft-ietf-oauth-browser-based-apps-26
"This file