← All articles

Frontend Security Beyond GDPR: CSP, XSS, Dependency Auditing, and Supply Chain Risk

Privacy law compliance and security engineering are different disciplines. A codebase can be fully GDPR-compliant and trivially exploitable at the same time. Here are the defaults every frontend lead should have shipped already.

Most frontend teams have thought carefully about user data and privacy. Far fewer have thought carefully about the attack surface of the JavaScript they ship. These are separate concerns, and conflating them is how XSS vulnerabilities end up in production behind GDPR-compliant consent banners.

This article covers the security layer: what to configure, why each setting exists, and what it actually prevents.

XSS in React: When the Framework Does Not Save You

React escapes content by default, which prevents the most common XSS vectors. But there are three specific patterns where the protection disappears entirely.

dangerouslySetInnerHTML

The name is the warning. If you are passing user-controlled content here, you have an XSS vulnerability:

// VULNERABLE — never pass unsanitized user content here
function RichContent({ html }: { html: string }) {
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

// SAFE — sanitize with DOMPurify before rendering
import DOMPurify from 'dompurify';

function RichContent({ html }: { html: string }) {
  const clean = DOMPurify.sanitize(html, {
    ALLOWED_TAGS: ['p', 'strong', 'em', 'ul', 'ol', 'li', 'a'],
    ALLOWED_ATTR: ['href'],
  });
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

href injection

React does not sanitize href attributes. A javascript: URL in a link executes when the user clicks it:

// VULNERABLE — user-supplied href can be javascript:alert(1)
function UserLink({ href, label }: { href: string; label: string }) {
  return <a href={href}>{label}</a>;
}

// SAFE — validate scheme before rendering
const SAFE_SCHEMES = ['https:', 'http:', 'mailto:'];

function UserLink({ href, label }: { href: string; label: string }) {
  let url: URL;
  try {
    url = new URL(href);
  } catch {
    return <span>{label}</span>; // invalid URL — render as text
  }
  if (!SAFE_SCHEMES.includes(url.protocol)) return <span>{label}</span>;
  return <a href={href} rel="noopener noreferrer">{label}</a>;
}

Server-side rendering and hydration

When user data is serialized into a <script> tag for SSR hydration (common in Next.js for initial state), it must be JSON-serialized carefully. Unescaped </script> inside a string will break out of the script block:

// VULNERABLE — breaks if data contains "</script>"
const __INITIAL_STATE__ = ${JSON.stringify(data)};

// SAFE — escape the closing tag sequence
const __INITIAL_STATE__ = ${JSON.stringify(data).replace(/<\/script/gi, '<\\/script')};

Content Security Policy

CSP is an HTTP response header that tells the browser which sources are allowed to load scripts, styles, images, and other resources. A strict CSP makes XSS significantly harder to exploit even if a vulnerability exists — injected scripts cannot execute if the CSP blocks their source.

In Next.js, configure CSP in middleware:

// middleware.ts
import { NextResponse } from 'next/server';

const CSP = [
  "default-src 'self'",
  "script-src 'self' 'nonce-{NONCE}'",  // nonce replaces 'unsafe-inline'
  "style-src 'self' 'unsafe-inline'",    // needed for CSS-in-JS; remove if possible
  "img-src 'self' data: https:",
  "font-src 'self'",
  "connect-src 'self' https://api.bobkov.dev",
  "frame-ancestors 'none'",             // prevents clickjacking
  "base-uri 'self'",
  "form-action 'self'",
].join('; ');

export function middleware(request: NextRequest) {
  const nonce = crypto.randomUUID();
  const response = NextResponse.next();
  response.headers.set(
    'Content-Security-Policy',
    CSP.replace('{NONCE}', nonce)
  );
  return response;
}
Start in report-only mode Deploy with Content-Security-Policy-Report-Only first. This logs violations without blocking anything, so you can see what your real traffic would break before enforcing. Run in report-only for a week before switching to enforcement.

npm Supply Chain Risk

In June 2024, the polyfill.io CDN was acquired and began injecting malware into the polyfill script served to millions of sites. Any site loading cdn.polyfill.io/v3/polyfill.min.js was affected. The fix was trivial — stop loading scripts from untrusted CDNs — but only if you knew it was happening.

Two defences:

Subresource Integrity (SRI)

If you must load a third-party script, lock it to a specific hash. The browser refuses to execute the script if the content does not match:

<script
  src="https://cdn.example.com/lib.min.js"
  integrity="sha384-abc123..."
  crossorigin="anonymous"
></script>

Dependency auditing in CI

# .github/workflows/security.yml
- name: Audit dependencies
  run: npm audit --audit-level=high

# Also useful: check for known-malicious packages
- name: Socket security scan
  uses: SocketDev/socket-security-action@v1
  with:
    api-key: ${{ secrets.SOCKET_SECURITY_API_KEY }}
Important npm audit only checks for known CVEs. It does not detect newly malicious packages, typosquatting, or post-install scripts that exfiltrate data. Use a dedicated supply chain tool (Socket, Snyk, or similar) for full coverage.

Other Headers Worth Setting

These are low-effort, high-value HTTP headers that every production frontend should have:

// next.config.ts — security headers for all routes
const securityHeaders = [
  { key: 'X-Frame-Options',         value: 'DENY' },
  { key: 'X-Content-Type-Options',  value: 'nosniff' },
  { key: 'Referrer-Policy',         value: 'strict-origin-when-cross-origin' },
  { key: 'Permissions-Policy',
    value: 'camera=(), microphone=(), geolocation=()' },
];

const config = {
  async headers() {
    return [{ source: '/(.*)', headers: securityHeaders }];
  },
};

The Security Audit Checklist

We run this checklist on every new service before it reaches production:

  • CSP header present and enforced (not just report-only)
  • No dangerouslySetInnerHTML with user-controlled content
  • All href props from user data validated against a scheme allowlist
  • npm audit --audit-level=high passing in CI
  • No scripts loaded from third-party CDNs without SRI hashes
  • X-Frame-Options: DENY and X-Content-Type-Options: nosniff on all responses
  • Secrets not present in client-side bundles (checked with NEXT_PUBLIC_ prefix audit)

Key Takeaways

  • GDPR compliance does not imply security — they address different threat models
  • React's XSS protection has three well-known gaps: dangerouslySetInnerHTML, href injection, and SSR serialization
  • Deploy CSP in report-only mode first; enforce after validating against real traffic
  • Never load third-party scripts without SRI hashes or a supply chain monitoring tool
  • Security headers (X-Frame-Options, nosniff, Referrer-Policy) are five minutes of work with disproportionate defensive value