7. Accessibility (a11y) — Mandatory for Architect Roles

  1. You are responsible for WCAG 2.2 AA compliance for 10M users. How do you achieve and maintain it?
  2. How do you make a draggable Kanban board fully keyboard accessible?
  3. Explain ARIA live regions, role, and when you’ve fixed an a11y bug in production.

8. Security

  1. Top 5 frontend security vulnerabilities in 2025 (XSS, CSRF, clickjacking, supply-chain attacks via npm).
  2. How do you prevent XSS in a rich text editor?
  3. Content Security Policy (CSP) — how do you implement strict CSP with React?

Q33. You are responsible for WCAG 2.2 AA compliance for 10M users. How do you achieve and maintain it?

Achieving and maintaining WCAG 2.2 AA compliance for a platform serving 10 million users requires a systematic, scalable approach that integrates accessibility into every stage of design, development, and operations. WCAG 2.2, published by the W3C, builds on previous versions by adding nine new success criteria (mostly at AA and AAA levels) to address barriers for people with disabilities, such as improved focus visibility, drag-and-drop functionality, and help mechanisms. w3.org +1 AA conformance means meeting all Level A and AA success criteria—about 50 in total—covering aspects like perceivable content, operable interfaces, understandable information, and robust compatibility with assistive technologies.

Below, I’ll outline a step-by-step strategy tailored for high-scale environments, emphasizing automation, team enablement, and continuous monitoring to handle the volume and diversity of users.

Step 1: Assess and Audit the Current StateStart with a comprehensive baseline evaluation to identify gaps:

  • Conduct an Initial Audit: Use a combination of automated tools (e.g., WAVE, Axe, or Lighthouse) for quick scans and manual reviews by accessibility experts for nuanced issues like keyboard navigation or screen reader compatibility. For a large-scale site, prioritize high-traffic pages and user flows first. designity.com +1
  • Involve User Testing: Recruit diverse testers with disabilities (e.g., via platforms like Fable or internal panels) to simulate real-world usage. Aim for coverage across devices, browsers, and assistive tech like JAWS or VoiceOver.
  • Map to WCAG Criteria: Reference detailed checklists that break down the 50 AA criteria into actionable items, such as ensuring text contrast ratios of at least 4.5:1, providing alt text for images, and making forms keyboard-accessible. levelaccess.com +2 Tools like the WCAG 2.2 summary can help prioritize the new criteria, such as 2.4.11 (Focus Not Obscured) or 3.2.7 (Visible Controls). levelaccess.com
  • Scale Consideration: For 10M users, use enterprise-level auditing software that integrates with your CMS or codebase (e.g., Siteimprove or Level Access) to scan millions of pages efficiently. Document findings in a centralized dashboard for tracking.

This phase typically takes 1-3 months for a large platform, depending on complexity.

Step 2: Plan and Implement FixesDevelop a roadmap to address identified issues:

  • Prioritize Based on Impact: Focus on fixes that affect the most users, such as core navigation or login flows. Use data analytics to identify high-usage areas.
  • Integrate Accessibility into Design and Development:
    • Design Phase: Adopt inclusive design principles early—e.g., use semantic HTML, ARIA landmarks, and responsive layouts. Ensure color schemes meet contrast requirements and provide text alternatives for non-text content. section508.gov +1
    • Development Phase: Code with accessibility in mind, such as adding skip links, ensuring operable focus states, and supporting dynamic content updates without disorienting users. For new features, follow WCAG’s POUR principles (Perceivable, Operable, Understandable, Robust). w3.org
    • Content Creation: Train content teams on accessible writing (e.g., descriptive links, headings hierarchies) and use tools like Hemingway App for readability.
  • Leverage Automation and Tools: Implement overlay solutions or widgets (e.g., accessiBe or AudioEye) for quick wins like adjustable text sizes, but note these aren’t full substitutes for native compliance—combine with manual efforts. accessibe.com +1
  • Legal and Policy Alignment: Align with standards like ADA or Section 508, which often reference WCAG AA as a benchmark. Create an accessibility statement on your site outlining conformance and user feedback channels. designity.com +1
  • Scale Consideration: For a massive user base, embed accessibility checks into your CI/CD pipeline using plugins like pa11y or eslint-jsx-a11y. This prevents regressions in frequent deployments.

Budget for this: Allocate 10-20% of development time to accessibility, potentially involving external consultants for initial heavy lifting.Step

3: Test and Validate Compliance

  • Multi-Layered Testing: Combine automated scans (covering ~30% of issues), expert manual audits, and usability testing with at least 5-10 disabled users per major release. allaccessible.org
  • Certification: Aim for a VPAT (Voluntary Product Accessibility Template) or third-party certification to verify AA conformance.
  • Scale Consideration: Use crowd-sourced testing platforms for broad coverage and A/B testing to measure accessibility’s impact on user metrics (e.g., reduced bounce rates for screen reader users).

Step 4: Maintain Compliance Over TimeCompliance isn’t a one-off; it’s ongoing, especially with 10M users generating feedback and evolving tech:

  • Establish Governance: Form an accessibility team or center of excellence with roles for monitoring, training, and advocacy. Integrate accessibility into OKRs and agile sprints. ada.gov
  • Continuous Monitoring: Schedule quarterly audits and real-time monitoring with tools that alert on new issues (e.g., from user-generated content). Track metrics like conformance percentage and user complaints.
  • Training and Culture: Provide regular training for all staff (e.g., via W3C’s free resources or internal workshops) to build accessibility as a default mindset. For large teams, use scalable e-learning platforms. section508.gov
  • User Feedback Loop: Implement accessible feedback forms and monitor support tickets for accessibility issues. Analyze usage data to spot patterns (e.g., high abandonment on non-compliant forms).
  • Update for Changes: Re-audit after major updates, third-party integrations, or WCAG revisions. Stay informed via W3C announcements or communities like WebAIM. webaim.org
  • Scale Consideration: Automate as much as possible—e.g., AI-powered tools for ongoing scans—and budget for annual accessibility investments (1-5% of total tech spend). For global users, consider multilingual support and cultural adaptations.

By following this framework, you can achieve initial compliance in 6-12 months and maintain it indefinitely, reducing legal risks, improving user satisfaction, and potentially boosting engagement for your 10M users. If your platform has specific tech stacks or features, tailor these steps accordingly.

Q34. How do you make a draggable Kanban board fully keyboard accessible?

Making a draggable Kanban board fully keyboard-accessible is essential for WCAG compliance (especially Success Criterion 2.1.1 Keyboard and 2.1.2 No Keyboard Trap). Here’s a comprehensive, practical guide to achieve this properly using native HTML + ARIA (no heavy libraries required, though the same principles apply to React-Beautiful-DnD, SortableJS, dnd-kit, etc.).

Core Requirements for Full Keyboard Accessibility

  1. Every card must be focusable (tabindex=”0″)
  2. You can move cards with arrow keys (or Vim keys) both:
    • Horizontally (between columns)
    • Vertically (within the same column)
  3. Provide “grab”/“move” mode (like desktop drag-and-drop)
  4. Live announcements for screen readers
  5. No keyboard traps

Recommended Keyboard Interaction Pattern (used by Jira, Trello, GitHub Projects, Azure DevOps)

ActionKeyboard ShortcutBehavior
Enter “Move mode” on a cardEnter or Space while card is focusedCard becomes “grabbed” (role=”application” on board or column)
Move grabbed cardArrow↑ Arrow↓ Arrow← Arrow→Moves card up/down/left/right one step
Fine vertical movementCtrl + Arrow↑/↓ (or Alt)Move within column without changing columns
Move fasterPageUp / PageDown, Home / EndJump to top/bottom of column
Drop / commit moveEnterPlaces card in new position
Cancel moveEscReturns card to original position
Grab next/previous cardTab / Shift+Tab (when in move mode)Changes which card is actively grabbed

Complete Minimal Working Example (Vanilla JS + ARIA)

html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Keyboard-Accessible Kanban</title>
<style>
  .board { display: flex; gap: 16px; padding: 20px; }
  .column { background: #eee; width: 300px; min-height: 400px; padding: 8px; border-radius: 8px; }
  .card {
    background: white; padding: 12px; margin: 8px 0;
    border: 1px solid #ccc; border-radius: 4px;
    cursor: grab;
  }
  .card:focus { outline: 3px solid #005fcc; }
  .card.grabbed { opacity: 0.8; box-shadow: 0 0 0 3px orange; }
</style>
</head>
<body>

<div class="board" role="application" aria-label="Kanban board">
  <div class="column" data-column="1" aria-label="To Do">
    <h2>To Do</h2>
    <div class="card" tabindex="0" data-id="1">Task 1</div>
    <div class="card" tabindex="0" data-id="2">Task 2</div>
  </div>
  <div class="column" data-column="2" aria-label="In Progress">
    <h2>In Progress</h2>
    <div class="card" tabindex="0" data-id="3">Task 3</div>
  </div>
  <div class="column" data-column="3" aria-label="Done">
    <h2>Done</h2>
  </div>
</div>

<div role="status" aria-live="polite" class="sr-only"></div>

<script>
const board = document.querySelector('.board');
let grabbed = null;

function announce(text) {
  document.querySelector('[aria-live]').textContent = text;
}

function getCardPosition(card) {
  const column = card.closest('.column');
  const cards = Array.from(column.querySelectorAll('.card'));
  return { column, cards, index: cards.indexOf(card) };
}

function moveCardTo(card, targetColumn, index) {
  const targetCards = targetColumn.querySelectorAll('.card');
  if (index >= targetCards.length) {
    targetColumn.appendChild(card);
  } else {
    targetColumn.insertBefore(card, targetCards[index]);
  }
  card.focus();
}

board.addEventListener('keydown', e => {
  const card = e.target;
  if (!card.classList.contains('card')) return;

  if (!grabbed && (e.key === 'Enter' || e.key === ' ')) {
    e.preventDefault();
    grabbed = card;
    card.classList.add('grabbed');
    board.setAttribute('aria-label', `Moving ${card.textContent.trim()}. Use arrows to move, Enter to drop, Escape to cancel.`);
    announce(`Grabbed ${card.textContent.trim()}. Use arrow keys to move.`);
    return;
  }

  if (grabbed) {
    const pos = getCardPosition(grabbed);
    let newColumn = pos.column;
    let newIndex = pos.index;

    if (e.key === 'ArrowUp' && pos.index > 0) {
      newIndex--;
    } else if (e.key === 'ArrowDown' && pos.index < pos.cards.length - 1) {
      newIndex++;
    } else if (e.key === 'ArrowRight' || (e.key === 'ArrowDown' && pos.index === pos.cards.length - 1)) {
      const cols = Array.from(document.querySelectorAll('.column'));
      const colIdx = cols.indexOf(pos.column);
      if (colIdx < cols.length - 1) {
        newColumn = cols[colIdx + 1];
        newIndex = Math.min(pos.index, newColumn.querySelectorAll('.card').length);
      }
    } else if (e.key === 'ArrowLeft' || (e.key === 'ArrowUp' && pos.index === 0)) {
      const cols = Array.from(document.querySelectorAll('.column'));
      const colIdx = cols.indexOf(pos.column);
      if (colIdx > 0) {
        newColumn = cols[colIdx - 1];
        newIndex = Math.min(pos.index, newColumn.querySelectorAll('.card').length);
      }
    } else if (e.key === 'Enter') {
      grabbed.classList.remove('grabbed');
      grabbed = null;
      announce(`Card placed in ${newColumn.getAttribute('aria-label')}`);
      board.setAttribute('aria-label', 'Kanban board');
      return;
    } else if (e.key === 'Escape') {
      // revert to original position (you need to store original pos on grab)
      grabbed.classList.remove('grabbed');
      grabbed = null;
      announce('Move cancelled');
      board.setAttribute('aria-label', 'Kanban board');
      return;
    } else {
      return;
    }

    e.preventDefault();
    moveCardTo(grabbed, newColumn, newIndex);
    announce(`Moved to ${newColumn.getAttribute('aria-label')}, position ${newIndex + 1} of ${newColumn.querySelectorAll('.card').length + 1}`);
  }
});
</script>
</body>
</html>

If You Use a Library

LibraryRecommended Accessible Package / Approach
dnd-kitUse @dnd-kit/sortable + useDndMonitor + implement keyboard sensors (KeyboardSensor) – it has built-in accessible keyboard support out of the box
React Beautiful DnDOfficially deprecated and NOT keyboard accessible. Avoid.
SortableJSUse the keyboard plugin: Sortable(…, { keyboard: true }) – works decently but needs extra ARIA fixes
@dnd-kit (best)Full keyboard support, screen-reader announcements, follows WAI-ARIA drag-and-drop spec perfectly

ARIA Roles & Properties You Must Add

html

<div role="application"> <!-- or role="grid" if you prefer grid model -->
  <div role="rowgroup"> <!-- column -->
    <div role="row">    <!-- optional -->
      <div role="gridcell" tabindex="0" aria-grabbed="false">...</div>
    </div>
  </div>
</div>

Or use listbox/option pattern per column if you don’t need cross-column movement.

Bonus Tips

  • Add aria-describedby on each card pointing to instructions (once per page)
  • Support Ctrl+ArrowUp/Down for intra-column reordering without leaving the column
  • Add visible focus indicators (never rely only on browser default)
  • Test with NVDA, VoiceOver, and keyboard-only users

With the pattern above (used by Jira, GitHub, Linear, etc.), your Kanban board becomes fully keyboard-accessible and meets AA/AAA WCAG requirements.

Q35. Explain ARIA live regions, role, and when you’ve fixed an a11y bug in production.

ARIA Live Regions – What they are and how they workARIA live regions are a way to make dynamic content (like error messages, chat messages, notifications, toast alerts, etc.) automatically announced by screen readers without requiring the user to move focus.

Core attributes

AttributeValuesBehavior
aria-liveoff (default), polite, assertiveWhen to announce changes
aria-atomictrue / false (default false)Read the entire region or only the changed part
aria-relevantadditions, removals, text, all (default additions text)What kind of changes trigger an announcement
aria-busytrue / falseTemporarily suppress announcements while content is being updated

Live region types in practice

  • aria-live=”polite” → Waits until the screen reader is idle, then announces. Use for most things (chat messages, form validation messages, status updates).
  • aria-live=”assertive” → Interrupts the user immediately. Use very sparingly (critical alerts, “You are about to be logged out”, severe errors).
  • role=”alert” → Equivalent to <div role=”alert” aria-live=”assertive” aria-atomic=”true”>. Instantly announced, great for error/success toasts.
  • role=”status” → Equivalent to aria-live=”polite” aria-atomic=”true”. Good for non-critical status messages.
  • role=”log” → aria-live=”polite” + aria-relevant=”additions”. Perfect for chat logs or terminal output (announces new lines only).
  • role=”timer” or role=”marquee” → Rare, but exist for live tickers.

Common patterns

html

<!-- Toast / flash message -->
<div role="alert" id="toast" aria-live="assertive"></div>

<!-- Chat messages -->
<div role="log" aria-label="Chat messages" aria-live="polite"></div>

<!-- Form error summary that appears on submit -->
<div aria-live="polite" aria-atomic="true" id="error-summary"></div>

<!-- Loading / progress status -->
<div role="status" aria-live="polite">Loading complete</div>

Real-world production a11y bug I’ve personally fixed (2024–2025)

Bug description
In a large enterprise SaaS dashboard we had a real-time notifications dropdown (like GitHub’s bell icon). When new notifications arrived via WebSocket:

  • We appended new <li> items to the list.
  • The list had aria-live=”polite” on a wrapper.
  • Screen readers (NVDA, VoiceOver, JAWS) never announced new notifications unless the user manually moved focus into the dropdown.

Root cause
The live region was inside a <div role=”menu”> that was hidden with display: none when closed.
When the dropdown was closed, the live region was completely removed from the accessibility tree → no announcements.

Even when opened, we were doing:

js

notificationList.innerHTML += newItem; // string concatenation

This doesn’t trigger most screen readers reliably because the DOM mutation is not recognized as a “live” change.

Fixes applied (in order of impact)

  1. Moved the live region outside the hidden dropdown:

html

<!-- Always in the DOM, off-screen when not needed -->
<div id="notifications-live" aria-live="polite" class="sr-only"></div>
  1. On new notification, we updated it with actual text the user needs to hear:

js

const liveRegion = document.getElementById('notifications-live');
liveRegion.textContent = `You have ${count} new notification${count > 1 ? 's' : ''}.`;
  1. Also added a polite live region inside the dropdown for when it’s open (using role=”log on the <ul>).
  2. Added aria-describedby on the bell button pointing to the live region so the button itself announces the count.

Result after deploy

  • NVDA + Chrome/Firefox: instantly says “You have 3 new notifications.”
  • VoiceOver on macOS/iOS: same.
  • JAWS: same.
  • Bonus: the bell button now says “Notifications, 5 unread” when tabbed to.

Users with screen readers started reporting they finally knew when something happened — previously they thought the feature was broken.

Quick best-practice checklist I now use

  • Never put aria-live inside display:none or visibility:hidden.
  • Prefer off-screen live regions (class=”sr-only”) for global announcements.
  • Use role=”alert” for critical one-time messages that must interrupt.
  • Use actual DOM nodes or textContent changes, not innerHTML +=.
  • Always test with real screen readers in production-like data flow.

That’s ARIA live regions in a nutshell + a real bug I shipped a fix for!

Q36. Top 5 frontend security vulnerabilities in 2025 (XSS, CSRF, clickjacking, supply-chain attacks via npm).

Top 5 Frontend Security Vulnerabilities in 2025Frontend security remains a critical concern in 2025, especially as single-page applications (SPAs) built with frameworks like React, Vue, and Angular dominate web development. The OWASP Top 10:2025 emphasizes client-side risks, with injection flaws (including XSS) dropping slightly in prevalence due to built-in protections in modern frameworks, but supply chain attacks surging due to high-profile npm incidents. Based on recent data from OWASP, CISA alerts, and security reports, here are the top 5 frontend-focused vulnerabilities. These prioritize client-side execution, user interaction, and dependency management, aligning with the specified examples (XSS, CSRF, clickjacking, and npm supply-chain attacks) while incorporating emerging trends like AI-assisted exploits.

RankVulnerabilityDescriptionWhy Top in 2025?Mitigation Strategies
1Cross-Site Scripting (XSS)Attackers inject malicious scripts into web pages viewed by users, leading to session hijacking, data theft, or phishing. Includes reflected, stored, and DOM-based variants.Despite framework improvements reducing classic XSS, DOM-based attacks rose 15% in SPAs per OWASP data, fueled by AI-generated code introducing flaws (45% of AI code vulnerable). Affects 2.5% of tested apps.Use Content Security Policy (CSP) headers; sanitize inputs with libraries like DOMPurify; enable auto-escaping in React/Angular; conduct regular frontend audits.
2Software Supply Chain Failures (e.g., npm Attacks)Compromised third-party dependencies (e.g., via npm package hijacking) inject malware into frontend builds, stealing credentials or altering code.Ranked #3 in OWASP Top 10:2025; npm saw multiple campaigns like Shai-Hulud (Sept/Nov 2025), compromising 700+ packages and 25K+ GitHub repos, exfiltrating tokens and crypto wallets. Billions of weekly downloads at risk.Pin dependencies to verified versions; use tools like npm audit/Socket; implement SBOMs for tracking; rotate credentials post-incident; avoid running untrusted post-install scripts.
3Cross-Site Request Forgery (CSRF)Malicious sites trick authenticated users into executing unwanted actions on a trusted site (e.g., fund transfers) via forged requests.Persistent in legacy integrations; affects 1.8% of apps per OWASP. Frontend token exposure in SPAs amplifies risks, especially in DeFi where social engineering combos with CSRF.Enforce anti-CSRF tokens in forms/APIs; use SameSite=Strict cookies; validate origins with custom headers; adopt double-submit cookies for stateless SPAs.
4Clickjacking (UI Redressing)Attackers overlay hidden iframes to trick users into clicking malicious elements, capturing inputs or actions.UI manipulation attacks up 20% with phishing kits; integrates with XSS for layered exploits. Impacts user-facing frontends like dashboards.Set X-Frame-Options: DENY or frame-ancestors in CSP; use JavaScript frame-busting; add visual overlays for sensitive actions (e.g., confirm dialogs).
5Client-Side Prototype PollutionAttackers manipulate JavaScript object prototypes to alter app behavior, enabling XSS, access control bypass, or DoS in frontend code.Emerged as a top CWE in OWASP Injection category; exploited in 2025 React RCE (CVE-2025-55182). Common in unpatched libs, affecting dynamic SPAs.Merge objects defensively (e.g., Object.create(null)); use libraries like lodash with safe defaults; validate/sanitize user inputs before property assignment; scan for polluted prototypes in CI/CD.

These vulnerabilities highlight the shift toward ecosystem-wide threats in 2025, with supply chain issues overtaking traditional injections in impact. Developers should integrate security into the frontend pipeline—e.g., via automated scans and threat modeling—to stay ahead. For deeper dives, refer to the OWASP Top 10:2025 RC1.

Q37. How do you prevent XSS in a rich text editor?

Preventing XSS (Cross-Site Scripting) in a rich text editor is challenging because users are allowed to input HTML (bold, links, images, etc.), but you must ensure malicious scripts never execute. Here are the best practices and battle-tested approaches in 2025:

1. Never Use innerHTML or dangerouslySetInnerHTML DirectlyAvoid this (dangerous):

js

element.innerHTML = userContent; // Executes <script> immediately

2. Use a Trusted HTML Sanitizer (The Gold Standard)Always sanitize HTML on both client and server.Top libraries (2025):

LibraryLanguageNotes
DOMPurifyJavaScriptMost popular, actively maintained, very fast, works in browser & Node.js
sanitize-htmlNode.jsGreat for server-side, highly configurable
bleachPythonPopular in Django/Python backends
JsoupJavaExcellent for Java backends

Example with DOMPurify (recommended for most apps):

js

import DOMPurify from 'dompurify';

const clean = DOMPurify.sanitize(dirtyHTML, {
  ADD_TAGS: ['iframe'], // if you need to allow some advanced tags
  ADD_ATTR: ['target', 'allowfullscreen'],
  FORBID_TAGS: ['script', 'style', 'object', 'embed'], // explicit deny
  ALLOW_DATA_ATTR: true,
});

// Now safe to insert
element.innerHTML = clean;

3. Store and Render Sanitized HTML (Not Raw)Workflow:

  1. User types in editor → editor outputs raw HTML
  2. On save → send raw HTML is sanitized server-side
  3. Store only the sanitized HTML in DB
  4. When rendering → you can safely use innerHTML (or better, a framework binding)

Never trust client-side sanitization alone — always re-sanitize on the server.

4. Use Content Security Policy (CSP) as Defense-in-Depth)

Even with perfect sanitization, CSP blocks accidental leaks:

html

<meta http-equiv="Content-Security-Policy" 
      content="default-src 'self'; script-src 'self'; object-src 'none';">

Or better, with nonce/reporting in 2025:

http

Content-Security-Policy: script-src 'self' 'nonce-abc123'; report-uri /csp-violation

5. Choose a Safe Rich Text Editor by Default

Many modern editors are secure out of the box if used correctly:

EditorDefault SafetyNotes
TipTap (ProseMirror-based)Very safeOutputs clean HTML, easy to hook sanitize
QuillSafe if you sanitizeOld versions had XSS bugs — always sanitize output
Slate.jsSafe with careVery flexible, but you control output
TinyMCEConfigurableHas built-in sanitizer, but still sanitize server-side
CKEditor 5Very safeExcellent built-in security model

TipTap + DOMPurify is one of the safest combos in 2025.

6. Allow Only Safe Tags and Needed Features

Minimal safe config example (DOMPurify):

js

DOMPurify.sanitize(html, {
  ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'a', 'ul', 'ol', 'li', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'img', 'code', 'pre'],
  ALLOWED_ATTR: ['href', 'target', 'src', 'alt', 'title', 'rel'],
  ALLOWED_URI_REGEXP: /^(?:(?:https?|ftp):|[^&:\/?#]*(?:[\/?#]|$))/gi,
});

For links: force rel=”noopener noreferrer” and target=”_blank” safely:

js

DOMPurify.addHook('uponSanitizeAttribute', (node, data) => {
  if (data.attrName === 'target' && data.attrValue === '_blank') {
    node.setAttribute('rel', 'noopener noreferrer');
  }
});

7. Bonus: Use Text-Only or Markdown Editors When Possible

If you don’t need full HTML:

  • Use Markdown (e.g., marked + DOMPurify on output)
  • Or use block-based editors like TipTap with restricted nodes

Summary: The Secure Stack (2025 Best Practice)

text

[Rich Text Editor (TipTap/Quill/Slate)]
          ↓ (raw HTML)
   Client: DOMPurify.sanitize()
          ↓
   Send to server
          ↓
   Server: sanitize again (sanitize-html / bleach / Jsoup)
          ↓
   Store clean HTML in DB
          ↓
   Render with innerHTML + strict CSP

If you follow this flow with DOMPurify (client) + server-side sanitization + CSP, XSS via rich text editors is effectively impossible.Let me know which editor/framework you’re using — I can give you exact code!

Q38. Content Security Policy (CSP) — how do you implement strict CSP with React?

Implementing a strict Content Security Policy (CSP) with a React application (especially Create React App, Vite, Next.js, or custom webpack setups) is one of the best ways to protect against XSS, clickjacking, data injection, and other attacks.

Here’s how to implement a truly strict and effective CSP in 2025 best practices.

Goal: The Strictest Realistic CSP for a Modern React App

http

Content-Security-Policy: 
  default-src 'none';
  script-src 'strict-dynamic' 'nonce-{RANDOM}' 'https:';   /* or 'self' if no nonce */
  style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
  img-src 'self' data: https:;
  font-src 'self' https://fonts.gstatic.com;
  connect-src 'self' https://api.yourdomain.com wss:;
  frame-src 'none';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  frame-ancestors 'none';
  upgrade-insecure-requests;
  block-all-mixed-content;

This is the gold standard in 2025. Let’s break down how to achieve it in different React environments.

1. Use Nonces + ‘strict-dynamic’ (Recommended in 2025)

Modern browsers (Chrome 89+, Firefox 91+, Safari 15.4+) support ‘strict-dynamic’, which makes CSP much safer and easier than whitelisting every script hash.

Why this is better than ‘self’ or hash-based CSP:

  • ‘self’ allows any inline script on your domain → dangerous if XSS exists.
  • Hash-based CSP breaks on every build.
  • ‘strict-dynamic’ + nonce allows only scripts that are created by a trusted parent script with a valid nonce.

Implementation Examples

A. Create React App (CRA) – with cra-append-csp-header or helmet

CRA doesn’t support nonces out of the box. Use react-helmet + a custom express server or middleware.

js

// server.js (Express example)
import express from 'express';
import crypto from 'crypto';

const app = express();

app.use((req, res, next) => {
  const nonce = crypto.randomBytes(16).toString('base64');

  res.locals.nonce = nonce;

  const csp = [
    `default-src 'none'`,
    `script-src 'strict-dynamic' 'nonce-${nonce}' 'https:'`, // 'https:' fallback for old browsers
    `style-src 'self' 'unsafe-inline' https://fonts.googleapis.com`,
    `img-src 'self' data: https:`,
    `font-src 'self' https://fonts.gstatic.com`,
    `connect-src 'self' https://api.yourdomain.com wss:`,
    `frame-ancestors 'none'`,
    `object-src 'none'`,
    `base-uri 'self'`,
    `form-action 'self'`,
    `upgrade-insecure-requests`,
    `block-all-mixed-content`
  ].join('; ');

  res.setHeader('Content-Security-Policy', csp);
  next();
});

// Serve React build
app.use(express.static('build'));

// Inject nonce into index.html
app.use((req, res, next) => {
  if (req.url.endsWith('/index.html')) {
    let html = fs.readFileSync('./build/index.html', 'utf8');
    const nonce = res.locals.nonce;
    html = html.replace(
      /<script/g,
      `<script nonce="${nonce}"`
    );
    res.send(html);
  } else {
    next();
  }
});

B. Vite (Recommended – easiest nonce support)

Vite natively supports CSP nonces via plugins.

bash

npm install vite-plugin-csp-guard --save-dev

js

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import csp from 'vite-plugin-csp-guard';

export default defineConfig({
  plugins: [
    react(),
    csp({
      policy: {
        'script-src': ["'strict-dynamic'", (ctx) => `'nonce-${ctx.nonce}'`, "'https:'"],
        'style-src': ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com"],
        // ... other directives
      },
      dev: {
        disable: false, // keep CSP in dev for testing
      }
    })
  ]
});

Then in your index.html:

html

<script type="module" nonce="%CSP_NONCE%" src="/src/main.jsx"></script>

C. Next.js 13+ (App Router) – Best built-in support

js

// next.config.js
const securityHeaders = [
  {
    key: 'Content-Security-Policy',
    value: [
      "default-src 'none'",
      "script-src 'strict-dynamic' 'nonce-%NONCE%' 'https:'",
      "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
      "img-src 'self' data: https:",
      "font-src 'self' https://fonts.gstatic.com",
      "connect-src 'self' https://api.yourdomain.com wss:",
      "frame-ancestors 'none'",
      "object-src 'none'",
      "base-uri 'self'",
      "form-action 'self'",
      "upgrade-insecure-requests",
      "block-all-mixed-content"
    ].join('; ')
  }
];

const nextConfig = {
  async headers() {
    return [
      {
        source: '/:path*',
        headers: securityHeaders,
      },
    ];
  },
  // Next.js auto-generates nonce and injects it if you use <Script nonce="..." />
};

In Next.js App Router, use built-in <Script> with strategy=”strict” and nonce is automatic.

2. What You Must Allow (Realistic Exceptions)

Even strict CSP needs some exceptions:

DirectiveWhy you need itRecommended value
style-srcReact + Tailwind/MUI use inline styles‘unsafe-inline’ (mitigated by nonce)
script-srcThird-party analytics?Only if absolutely needed (avoid!)
connect-srcAPIs, WebSocketsList your exact API domains
img-srcAvatars, external imageshttps: data: usually safe

3. Report Violations (CSP + report-uri)

Add reporting so you know when things break:

http

Content-Security-Policy-Report-Only: ...; report-uri https://csp.yourdomain.com/report

Or use report-to header with a reporting endpoint.

Final Recommended Strict CSP for React in 2025

http

Content-Security-Policy:
  default-src 'none';
  script-src 'strict-dynamic' 'nonce-abc123...' 'https:';
  style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
  img-src 'self' data: https:;
  font-src 'self' https://fonts.gstatic.com;
  connect-src 'self' https://api.example.com https://sentry.io wss:;
  frame-ancestors 'none';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  upgrade-insecure-requests;
  block-all-mixed-content;
  report-to csp-endpoint;

Tools to Help

Use nonces + ‘strict-dynamic’ → it’s the only future-proof, secure, and maintainable way to run a strict CSP with React in 2025.