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;
}
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 }}
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
dangerouslySetInnerHTMLwith user-controlled content - All
hrefprops from user data validated against a scheme allowlist npm audit --audit-level=highpassing in CI- No scripts loaded from third-party CDNs without SRI hashes
X-Frame-Options: DENYandX-Content-Type-Options: nosniffon 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,hrefinjection, 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