Content security policy

Start from default-src 'self' and allow only what you load.

helmet.jshelmetjs.github.io
helmet.js
ReferenceRead docs ↗

From security-best-practices: Helmet config, OWASP A05.

Build the header

Toggle directives until all ten land as intended.

No CSPBad
(no Content-Security-Policy header)
    10/10 load · 3 unwanted
    Your CSPGood
    Directives
    
              
      import helmet from "helmet";
      
      app.use(helmet({
        contentSecurityPolicy: {
          directives: {
            defaultSrc: ["'self'"],
            scriptSrc: ["'self'", "https://trusted-cdn.com"],
            styleSrc: ["'self'"],
            imgSrc: ["'self'", "data:", "https:"],
            connectSrc: ["'self'", "https://api.example.com"],
            fontSrc: ["'self'", "https:", "data:"],
            objectSrc: ["'none'"],                              // 'self' would still run an uploaded .swf
            mediaSrc: ["'self'"],
            frameSrc: ["'none'"],                               // 'self' would still frame an uploaded .html
          },
        },
      }));

      Fire an injection

      Pick a payload, then edit the policy.

        // next.config.js
        const csp = [
          "default-src 'self'",
          "script-src 'self' https://trusted-cdn.com",
          "img-src 'self' data: https:",
          "connect-src 'self' https://api.example.com",
          "object-src 'none'",
          "frame-src 'none'",
        ].join("; ");
        
        module.exports = {
          async headers() {
            return [{ source: "/(.*)", headers: [{ key: "Content-Security-Policy", value: csp }] }];
          },
        };

        'unsafe-inline' reopens XSS

        Type or load an injected comment.

        Nonce
        'unsafe-inline'Bad
        
                  
          NonceGood
          
                    
            import crypto from "node:crypto";
            
            app.use((req, res, next) => {
              res.locals.nonce = crypto.randomBytes(16).toString("base64"); // new on every response
              next();
            });
            
            app.use(helmet({
              contentSecurityPolicy: {
                directives: {
                  scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.nonce}'`, "https://trusted-cdn.com"],
                },
              },
            }));
            
            // <script nonce="<%= nonce %>">boot()</script>

            Why it matters, pros and cons

            Why it matters

            The browser enforces the allow-list, so one missed escape can't run script.

            Pros

            • Injected scripts from unknown origins are refused.
            • connect-src limits where stolen data goes.
            • Report-Only mode rolls it out safely.

            Cons

            • Every new third party needs a policy edit.
            • 'unsafe-inline' undoes most XSS protection; use nonces.
            • Defense in depth only; escaping output is the fix.