Skip to content

Security: ChefJulio/sovereign-react

Security

docs/security.md

Security

Security patterns for client-side applications.


Client-Side Security Model

Running entirely in the browser means:

  • No server-side vulnerabilities (SQL injection, SSRF, etc.)
  • No user data transmitted over the network
  • No authentication tokens to protect

But client-side apps still face:

  • XSS (Cross-Site Scripting)
  • Unsafe dynamic code execution
  • Clipboard/file injection
  • Third-party script risks

Content Security Policy (CSP)

Configure CSP headers in your hosting platform. This is your primary defense against XSS.

// Example: vercel.json
{
  "headers": [
    {
      "source": "/(.*)",
      "headers": [
        {
          "key": "Content-Security-Policy",
          "value": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; connect-src 'self'; worker-src 'self' blob:; object-src 'none'; frame-ancestors 'none'"
        }
      ]
    }
  ]
}

Key directives:

  • script-src 'self' -- Only run scripts from your domain
  • object-src 'none' -- Block Flash/Java plugins
  • frame-ancestors 'none' -- Prevent clickjacking (embedding in iframes)

When to relax CSP: If you load fonts from Google Fonts, scripts from a CDN, or images from external sources, add those specific domains. Never use unsafe-eval or wildcard *.


Input Sanitization

HTML Content (DOMPurify)

Any time you render user-provided content as HTML, sanitize it:

import DOMPurify from 'dompurify';

// Safe
<div dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(userInput) }} />

// UNSAFE - never do this
<div dangerouslySetInnerHTML={{ __html: userInput }} />

When this matters: Markdown previews, rich text editors, HTML entity tools, any feature that renders user input as markup.

Dynamic Code Execution

If a feature evaluates user expressions (calculator, formula parser):

// UNSAFE
const result = eval(userInput);

// SAFER -- token allowlist
const ALLOWED_TOKENS = /^[0-9+\-*/().%\s]+$/;
if (!ALLOWED_TOKENS.test(userInput)) {
  throw new Error('Invalid expression');
}
const result = new Function(`return (${userInput})`)();

Even with new Function(), validate the input against an allowlist of expected tokens first. Never pass raw user input to any code execution function.


Cryptographic Operations

For features that involve hashing, encryption, or secure random generation:

DO:

// Use Web Crypto API
const hash = await crypto.subtle.digest('SHA-256', data);
const key = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);

// Use crypto.getRandomValues for secure randomness
const array = new Uint8Array(32);
crypto.getRandomValues(array);

DON'T:

// Never use Math.random() for security
const password = Math.random().toString(36);  // PREDICTABLE

// Never implement custom crypto
const encrypted = text.split('').reverse().join('');  // NOT ENCRYPTION

File Upload Validation

When features accept file uploads:

// Validate MIME type
const allowedTypes = /^image\/(png|jpeg|gif|webp)$/;
if (!allowedTypes.test(file.type)) {
  throw new Error('Invalid file type');
}

// Validate file size
const MAX_SIZE = 10 * 1024 * 1024; // 10MB
if (file.size > MAX_SIZE) {
  throw new Error('File too large');
}

The useFileUpload hook handles both validations. See Hooks Reference.


Additional Security Headers

Beyond CSP, configure these headers on your hosting platform:

Header Value Purpose
X-Content-Type-Options nosniff Prevent MIME type sniffing
X-Frame-Options DENY Prevent clickjacking
Referrer-Policy strict-origin-when-cross-origin Limit referrer information
Permissions-Policy camera=(), microphone=(), geolocation=() Disable unused browser APIs

Note: Only restrict APIs you don't use. If your app needs camera access (e.g., QR scanner), don't block it in Permissions-Policy.


What NOT to Store

  • Passwords or secrets in localStorage (readable by any script on the same origin)
  • API keys in client-side code (visible in source/network tab)
  • Encryption keys in localStorage (derive from user password instead)

If a feature encrypts user data (e.g., a secure notes tool), derive the key from a user-provided password using PBKDF2 and never persist the key itself.

There aren’t any published security advisories