Problem Statement
Custom text values configured for node labels and network descriptions can serve as cross-site scripting (XSS) vectors. Node operators can set arbitrary displayName and description fields on their validator nodes via the configuration API. Without proper sanitization on the frontend, a malicious operator could inject <script> tags or onerror handlers into these fields, which would execute in the context of every other operator viewing the node dashboard, potentially exfiltrating session cookies or wallet keys.
Technical Bounds & Invariants
- Input sources: node display name, node description, location string, contact email, website URL
- Render contexts: HTML (React JSX),
dangerouslySetInnerHTML is never used (must be banned)
- Character encoding: UTF-8 with full Unicode support (emoji, CJK, RTL scripts allowed)
- Sanitization must preserve legitimate Unicode and common formatting characters
- Performance impact: sanitization must add less than 1ms per rendered string
Codebase Navigation Guide
- Primary target:
/src/components/network/NodeCard.tsx
- Node data interface:
/src/types/node.ts — Node.displayName, Node.description, Node.location
- List rendering:
/src/components/network/NodeList.tsx
- Detail view:
/src/components/network/NodeDetailPanel.tsx
Step-by-Step Resolution Blueprint
- Install and configure
DOMPurify as the sanitization library with strict settings: DOMPurify.sanitize(input, { ALLOWED_TAGS: [], ALLOWED_ATTR: [] }) — no HTML tags or attributes are permitted; plain text only
- Create a utility function
sanitizeNodeField(input: string, field: 'displayName' | 'description' | 'location' | 'contactEmail' | 'websiteUrl') -> string in /src/utils/sanitize.ts that: (a) strips HTML tags via DOMPurify, (b) normalizes Unicode to NFC form via input.normalize('NFC'), (c) removes control characters except \n, \t, and printable Unicode via regex /[^\S\n\t\w\s\p{L}\p{N}\p{P}\p{Sc}]/gu, (d) trims whitespace, (e) enforces per-field max lengths (displayName: 50 chars, description: 500 chars, etc.)
- Add a custom ESLint rule in
eslint.config.js that flags any use of dangerouslySetInnerHTML across the entire codebase and fails the CI build
- Create a
SafeText component: <SafeText text={rawString} maxLength={50} /> that applies sanitizeNodeField and renders the sanitized output inside a <span> with title attribute showing the full sanitized value for truncation cases
- Add a CSP (Content Security Policy) header in
next.config.js: script-src 'self'; object-src 'none'; to provide a defense-in-depth layer even if sanitization fails
- Write a property-based test using
fast-check that generates 500 random malicious strings (with embedded <script>, javascript:, onerror=, data:text/html, etc.) and asserts the output of sanitizeNodeField contains no angle brackets, no event handlers, and no protocol-based XSS vectors
- Add a regression test in Playwright that: (a) uses the API to set a node's display name to
<img src=x onerror=alert(1)>, (b) navigates to the NodeCard, (c) asserts no alert dialog appears (using page.on('dialog') with a fail handler)
Problem Statement
Custom text values configured for node labels and network descriptions can serve as cross-site scripting (XSS) vectors. Node operators can set arbitrary
displayNameanddescriptionfields on their validator nodes via the configuration API. Without proper sanitization on the frontend, a malicious operator could inject<script>tags oronerrorhandlers into these fields, which would execute in the context of every other operator viewing the node dashboard, potentially exfiltrating session cookies or wallet keys.Technical Bounds & Invariants
dangerouslySetInnerHTMLis never used (must be banned)Codebase Navigation Guide
/src/components/network/NodeCard.tsx/src/types/node.ts—Node.displayName,Node.description,Node.location/src/components/network/NodeList.tsx/src/components/network/NodeDetailPanel.tsxStep-by-Step Resolution Blueprint
DOMPurifyas the sanitization library with strict settings:DOMPurify.sanitize(input, { ALLOWED_TAGS: [], ALLOWED_ATTR: [] })— no HTML tags or attributes are permitted; plain text onlysanitizeNodeField(input: string, field: 'displayName' | 'description' | 'location' | 'contactEmail' | 'websiteUrl') -> stringin/src/utils/sanitize.tsthat: (a) strips HTML tags via DOMPurify, (b) normalizes Unicode to NFC form viainput.normalize('NFC'), (c) removes control characters except\n,\t, and printable Unicode via regex/[^\S\n\t\w\s\p{L}\p{N}\p{P}\p{Sc}]/gu, (d) trims whitespace, (e) enforces per-field max lengths (displayName: 50 chars, description: 500 chars, etc.)eslint.config.jsthat flags any use ofdangerouslySetInnerHTMLacross the entire codebase and fails the CI buildSafeTextcomponent:<SafeText text={rawString} maxLength={50} />that appliessanitizeNodeFieldand renders the sanitized output inside a<span>withtitleattribute showing the full sanitized value for truncation casesnext.config.js:script-src 'self'; object-src 'none';to provide a defense-in-depth layer even if sanitization failsfast-checkthat generates 500 random malicious strings (with embedded<script>,javascript:,onerror=,data:text/html, etc.) and asserts the output ofsanitizeNodeFieldcontains no angle brackets, no event handlers, and no protocol-based XSS vectors<img src=x onerror=alert(1)>, (b) navigates to the NodeCard, (c) asserts noalertdialog appears (usingpage.on('dialog')with a fail handler)