Your strict CSP just nuked every Module Federation remote
Hardening Content Security Policies for Webpack Module Federation in Next.js
Current Situation Analysis
Modern frontend architectures increasingly rely on Module Federation to compose independent applications at runtime. This approach decouples deployment cycles, enables team autonomy, and reduces bundle duplication. However, when engineering teams attempt to align these dynamic architectures with enterprise security standards, a critical friction point emerges: Content Security Policy (CSP) enforcement.
The industry pain point is straightforward but costly. Security teams mandate strict CSP headers to mitigate cross-site scripting (XSS) and data injection attacks. Developers apply a hardened script-src 'self' directive, deploy to staging, and immediately observe a silent failure mode. The host shell renders correctly, but every federated remote container fails to mount. The UI displays blank placeholders, and the browser console logs two distinct violations:
Refused to load the script... violates script-src 'self'Refused to evaluate a string as JavaScript... 'unsafe-eval' is not allowed
This problem is consistently overlooked because most CSP documentation treats security headers as static asset controls. Engineers assume that whitelisting remote origins in script-src is sufficient. They miss the runtime mechanics of webpack's Module Federation implementation. The federation container bootstrap process does not merely fetch a remote entry file; it dynamically parses and executes the container manifest using eval() or new Function(). This design choice exists because webpack's runtime must resolve chunk graphs, handle version mismatches, and inject shared dependencies without synchronous blocking.
When a strict CSP strips unsafe-eval, the browser's security engine intercepts the runtime's evaluation call before any module can initialize. The result is a broken micro-frontend architecture that appears functional during local development (where CSP is often disabled or relaxed) but fails catastrophically in hardened environments. Compliance audits, penetration tests, and production rollouts all expose this gap, forcing teams to choose between security posture and architectural viability.
WOW Moment: Key Findings
The core insight is that Module Federation's runtime bootstrap mechanism fundamentally conflicts with zero-trust CSP configurations. The following comparison illustrates the operational trade-offs between a theoretically perfect security posture and a production-viable Module Federation setup.
| Approach | Remote Bootstrap Success | Security Posture | Dev Experience | Compliance Readiness |
|---|---|---|---|---|
Strict CSP (script-src 'self') |
Fails (eval blocked) | Maximum | Broken in staging/prod | High (but non-functional) |
MF-Compatible CSP (unsafe-eval + strict other directives) |
Succeeds | High (contained risk) | Stable across environments | Medium-High (auditable) |
| Nonce-Based CSP | Fails (injection timing mismatch) | Theoretical Maximum | Unstable | High (but incompatible) |
Why this finding matters: It shifts the security strategy from elimination to containment. Since webpack's Module Federation runtime requires dynamic evaluation, removing unsafe-eval is currently non-negotiable. The engineering focus must therefore pivot to minimizing the blast radius of that directive. By strictly scoping connect-src, frame-src, and img-src, implementing report-only rollout pipelines, and monitoring violation endpoints, teams can maintain enterprise-grade security while preserving runtime federation capabilities. Additionally, the nonce incompatibility with NextFederationPlugin confirms that strict CSP patterns cannot be forced onto the current webpack runtime architecture without upstream changes.
Core Solution
Implementing a production-ready CSP for Module Federation requires a structured approach that separates environment concerns, isolates directive scopes, and establishes a violation monitoring pipeline. The following implementation uses TypeScript and Next.js configuration patterns to demonstrate a scalable architecture.
Step 1: Environment-Aware Header Generation
Next.js allows dynamic header configuration via next.config.ts. Instead of hardcoding strings, we construct a factory function that merges base security directives with environment-specific overrides. This prevents accidental localhost whitelisting in production and ensures WebSocket protocols are only active during development.
import type { NextConfig } from 'next';
type CSPDirective = 'default-src' | 'script-src' | 'style-src' | 'img-src' | 'connect-src' | 'frame-src' | 'font-src' | 'object-src' | 'base-uri' | 'form-action';
interface CSPConfig {
isProduction: boolean;
remoteOrigins: string[];
analyticsEndpoints: string[];
paymentGateways: string[];
}
function buildCSPHeader(config: CSPConfig): string {
const { isProduction, remoteOrigins, analyticsEndpoints, paymentGateways } = config;
const devOverrides = isProduction ? [] : [
'http://localhost:*',
'ws://localhost:*',
'wss://localhost:*',
'https://localhost:*'
];
const scriptSources = [
"'self'",
"'unsafe-eval'",
"'unsafe-inline'",
...remoteOrigins,
...devOverrides
];
const connectSources = [
"'self'",
...analyticsEndpoints,
...remoteOrigins,
...devOverrides
];
const frameSources = [
"'self'",
...paymentGateways,
...devOverrides
];
const directives: Record<CSPDirective, string[]> = {
'default-src': ["'self'"],
'script-src': scriptSources,
'style-src': ["'self'", "'unsafe-inline'", ...devOverrides],
'img-src': ["'self'", 'data:', 'blob:', 'https:'],
'connect-src': connectSources,
'frame-src': frameSources,
'font-src': ["'self'", 'https://fonts.gstatic.com'],
'object-src': ["'none'"],
'base-uri': ["'self'"],
'form-action': ["'self'"]
};
return Object.entries(directives)
.map(([key, values]) => `${key} ${values.join(' ')}`)
.join('; ');
}
Step 2: Next.js Configuration Integration
The header builder integrates directly into the Next.js configuration. We expose both Content-Security-Policy and Content-Security-Policy-Report-Only headers to enable safe rollout. The report-only header allows browsers to log violations without blocking execution, which is critical during the initial deployment phase.
const nextConfig: NextConfig = {
async headers() {
const env = process.env.NODE_ENV;
const isProd = env === 'production';
const cspConfig: CSPConfig = {
isProduction: isProd,
remoteOrigins: [
'https://catalog.myapp.io',
'https://checkout.myapp.io',
'https://profile.myapp.io'
],
analyticsEndpoints: [
'https://analytics.provider.com',
'https://api.segment.io'
],
paymentGateways: [
'https://js.stripe.com',
'https://hooks.stripe.com'
]
};
const cspValue = buildCSPHeader(cspConfig);
return [
{
source: '/:path*',
headers: [
{
key: 'Content-Security-Policy-Report-Only',
value: cspValue
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'X-Frame-Options',
value: 'DENY'
},
{
key: 'X-XSS-Protection',
value: '1; mode=block'
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin'
},
{
key: 'Permissions-Policy',
value: 'camera=(), microphone=(), geolocation=()'
},
...(isProd ? [{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload'
}] : [])
]
}
];
}
};
export default nextConfig;
Step 3: Violation Logging Endpoint
CSP violations are useless without observability. We implement a Next.js API route that accepts POST requests from browsers reporting policy breaches. The endpoint parses the JSON payload, extracts the violated directive and blocked URI, and forwards the data to your monitoring system.
// pages/api/csp-report.ts
import type { NextApiRequest, NextApiResponse } from 'next';
export default function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const report = req.body['csp-report'] || req.body;
const { 'violated-directive': directive, 'blocked-uri': blockedUri, 'document-uri': docUri } = report;
// Forward to logging/monitoring system
console.warn('[CSP Violation]', {
directive,
blockedUri,
documentUri: docUri,
timestamp: new Date().toISOString()
});
res.status(204).end();
} catch (error) {
console.error('[CSP Report Parse Error]', error);
res.status(400).json({ error: 'Invalid report format' });
}
}
Architecture Rationale
- Directive Separation:
script-src,connect-src, andframe-srcare isolated because they govern different execution contexts. Mixing them creates overly permissive policies that defeat the purpose of CSP. - Environment Branching: Development servers rely on WebSocket hot-reload and localhost resolution. Production environments require HSTS and strict origin validation. Branching prevents accidental exposure of dev protocols in hardened deployments.
- Report-Only First: Switching directly to enforcement mode risks breaking third-party integrations or internal tooling. Report-only mode provides a 7-14 day audit window to identify false positives before enforcement.
- Nonce Incompatibility Acknowledgment: Strict CSP nonces require script tags to include a
nonce="..."attribute. Module Federation's runtime injects scripts dynamically after the initial HTML parse, making nonce matching impossible with current webpack implementations. The architecture acceptsunsafe-evalas a controlled risk while maximizing protection elsewhere.
Pitfall Guide
1. Premature Removal of unsafe-eval
Explanation: Engineers often strip unsafe-eval during security hardening without testing remote container initialization. Webpack's federation runtime uses dynamic evaluation to parse container manifests and resolve shared dependency graphs.
Fix: Maintain unsafe-eval in script-src until webpack or the federation plugin introduces a static bootstrap alternative. Compensate with strict connect-src and frame-src scoping.
2. Missing WebSocket Protocols in Development
Explanation: Next.js dev servers use ws:// or wss:// for hot module replacement. Omitting these protocols causes CSP violations that break live reloading, forcing developers to disable headers locally.
Fix: Conditionally inject ws://localhost:* and wss://localhost:* only when NODE_ENV !== 'production'.
3. Overlooking connect-src for External APIs
Explanation: script-src controls execution, but connect-src governs fetch(), XMLHttpRequest, and WebSocket connections. Analytics providers, payment processors, and telemetry endpoints fail silently if not whitelisted here.
Fix: Explicitly list all external API domains in connect-src. Use wildcard subdomains only when absolutely necessary, and prefer exact domain matches.
4. Assuming Strict Nonces Work with Federation Plugins
Explanation: Nonce-based CSPs require static script tags to carry a server-generated token. Module Federation injects remote entry points at runtime via JavaScript, bypassing the initial HTML parsing phase where nonces are validated. Fix: Do not attempt nonce enforcement for federated remotes. Rely on origin whitelisting and report-only monitoring instead.
5. Applying HSTS in Local Development
Explanation: Strict-Transport-Security forces HTTPS. Local development typically runs over HTTP. Applying HSTS locally breaks dev tools, proxy configurations, and localhost API calls.
Fix: Wrap HSTS header injection in a production-only conditional. Never ship HSTS to staging without verifying TLS certificate validity.
6. Over-Restricting frame-src for Payment Modals
Explanation: Payment gateways and identity providers often render checkout flows or SSO dialogs inside iframes. A restrictive frame-src 'self' policy blocks these embedded contexts, causing transaction failures.
Fix: Whitelist specific gateway domains in frame-src. Audit iframe sources quarterly to remove deprecated providers.
7. Ignoring CORS on the CSP Report Endpoint
Explanation: Browsers send CSP violation reports via POST requests. If the report endpoint lacks proper CORS headers or JSON parsing, violations are silently dropped, creating a false sense of security.
Fix: Ensure the API route accepts application/csp-report or application/json content types. Validate the payload structure before logging.
Production Bundle
Action Checklist
- Audit all remote container origins and list them explicitly in
script-src - Implement environment-conditional header generation in
next.config.ts - Deploy
Content-Security-Policy-Report-Onlyfor a minimum of 10 days - Create
/api/csp-reportendpoint with structured logging and alerting - Verify WebSocket protocols are excluded from production builds
- Test payment gateways and analytics endpoints against
connect-srcandframe-src - Switch to enforcement mode only after zero critical violations are logged
- Document
unsafe-evalusage in security architecture reviews with mitigation rationale
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|---|---|---|
| Local Development | Relaxed CSP with unsafe-eval, ws://, localhost:* |
Enables hot-reload and rapid iteration without header friction | Zero |
| Staging / QA | Report-only CSP with full production origins | Validates policy without blocking user flows or third-party scripts | Low (monitoring setup) |
| Production (Standard) | Enforcement CSP with unsafe-eval, strict connect-src/frame-src |
Balances security compliance with federation runtime requirements | Medium (audit overhead) |
| High-Security Compliance | Enforcement CSP + runtime integrity checks + strict origin validation | Meets regulatory requirements while acknowledging webpack limitations | High (engineering investment) |
Configuration Template
Copy this template into your Next.js project. Replace placeholder origins with your actual remote containers and third-party services.
// next.config.ts
import type { NextConfig } from 'next';
const REMOTE_CONTAINERS = [
'https://catalog.yourdomain.com',
'https://checkout.yourdomain.com'
];
const THIRD_PARTY_SERVICES = [
'https://analytics.yourdomain.com',
'https://js.stripe.com'
];
const nextConfig: NextConfig = {
async headers() {
const isProd = process.env.NODE_ENV === 'production';
const devSources = isProd ? [] : [
'http://localhost:*', 'ws://localhost:*', 'wss://localhost:*', 'https://localhost:*'
];
const csp = [
`default-src 'self'`,
`script-src 'self' 'unsafe-eval' 'unsafe-inline' ${REMOTE_CONTAINERS.join(' ')} ${devSources.join(' ')}`,
`style-src 'self' 'unsafe-inline' ${devSources.join(' ')}`,
`img-src 'self' data: blob: https:`,
`connect-src 'self' ${THIRD_PARTY_SERVICES.join(' ')} ${REMOTE_CONTAINERS.join(' ')} ${devSources.join(' ')}`,
`frame-src 'self' ${THIRD_PARTY_SERVICES.join(' ')} ${devSources.join(' ')}`,
`font-src 'self' https://fonts.gstatic.com`,
`object-src 'none'`,
`base-uri 'self'`,
`form-action 'self'`
].join('; ');
return [
{
source: '/:path*',
headers: [
{ key: 'Content-Security-Policy-Report-Only', value: csp },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-XSS-Protection', value: '1; mode=block' },
{ key: 'Referrer-Policy', value: 'strict-origin-when-cross-origin' },
{ key: 'Permissions-Policy', value: 'camera=(), microphone=(), geolocation=()' },
...(isProd ? [{ key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubDomains; preload' }] : [])
]
}
];
}
};
export default nextConfig;
Quick Start Guide
- Initialize Header Builder: Replace your static CSP strings with the environment-aware factory function. Define explicit arrays for remote containers and third-party services.
- Deploy Report-Only Mode: Apply the
Content-Security-Policy-Report-Onlyheader across all routes. Monitor the/api/csp-reportendpoint for 7-14 days. - Audit Violations: Review logged violations. Identify false positives from internal tooling or outdated third-party scripts. Update directive lists accordingly.
- Enforce Production Policy: Once violation logs stabilize, switch to the
Content-Security-Policyheader. Verify remote containers mount correctly and payment/analytics flows remain functional. - Establish Monitoring: Connect the CSP report endpoint to your observability stack. Set alerts for new blocked origins or directive mismatches to catch configuration drift early.
