Helmet itself doesn’t actually do anything to your Express app’s security; it’s a collection of middleware functions that configure HTTP headers. The real security comes from those headers, and Content Security Policy (CSP) is the most powerful one you’ll leverage.

Let’s see Helmet in action, and how it sets headers.

const express = require('express');
const helmet = require('helmet');

const app = express();

// Using helmet to set various security headers, including CSP
app.use(helmet());

// A basic CSP configuration
app.use(
  helmet.contentSecurityPolicy({
    directives: {
      defaultSrc: ["'self'"], // Only allow resources from the same origin
      scriptSrc: ["'self'", "https://cdnjs.cloudflare.com"], // Allow scripts from self and Cloudflare CDN
      styleSrc: ["'self'", "'unsafe-inline'"], // Allow styles from self and inline styles (use with caution!)
      imgSrc: ["'self'", "data:"], // Allow images from self and data URIs
      connectSrc: ["'self'"], // Allow connections to self
    },
  })
);

app.get('/', (req, res) => {
  res.send(`
    <!DOCTYPE html>
    <html>
    <head>
      <title>CSP Example</title>
      <link rel="stylesheet" href="/styles.css">
      <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
    </head>
    <body>
      <h1>Hello, World!</h1>
      <img src="/images/logo.png">
      <script>
        console.log('Inline script executed!');
      </script>
    </body>
    </html>
  `);
});

app.listen(3000, () => {
  console.log('Server listening on port 3000');
});

When you visit http://localhost:3000, your browser will receive headers like:

HTTP/1.1 200 OK
X-DNS-Prefetch-Control: off
X-Frame-Options: SAMEORIGIN
Strict-Transport-Security: max-age=15552000; includeSubDomains
X-Download-Options: no-open
X-Content-Type-Options: nosniff
X-XSS-Protection: 0
Referrer-Policy: no-referrer
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'

Notice the Content-Security-Policy header. This is the core of the security we’re configuring. It tells the browser exactly where it’s allowed to fetch resources from.

The problem CSP solves is a broad category of attacks, primarily Cross-Site Scripting (XSS) and data injection. By default, browsers trust any resource loaded by a webpage. An attacker can exploit this by injecting malicious scripts or directing the browser to load resources from attacker-controlled domains. CSP acts as a whitelist, allowing you to specify precisely which domains are permitted to serve scripts, styles, images, and other assets.

Internally, Helmet’s contentSecurityPolicy middleware takes a JavaScript object defining the policy. Each key in this object (like defaultSrc, scriptSrc, styleSrc) corresponds to a directive in the CSP header. The values are arrays of strings representing allowed sources.

  • defaultSrc: The fallback for other directives. If script-src isn’t defined, default-src applies to scripts.
  • scriptSrc: Controls allowed JavaScript sources. 'self' means the same origin as the document. 'unsafe-inline' allows inline <script> blocks (use sparingly!). 'unsafe-eval' allows eval() and similar constructs.
  • styleSrc: Controls allowed CSS sources. 'unsafe-inline' allows inline <style> blocks and style attributes.
  • imgSrc: Controls allowed image sources. data: allows images encoded as data URIs.
  • connectSrc: Controls origins that scripts can connect to (e.g., via fetch, XMLHttpRequest, WebSockets).

The most surprising thing about Content Security Policy is how granular you can get, and how many common development practices actually violate it by default. For example, many developers rely on inline event handlers like <button onclick="myFunction()">. CSP, by default, blocks these. To allow them, you’d need to add 'unsafe-inline' to your script-src directive, which significantly weakens the policy’s effectiveness against XSS. A better approach is to move all event handlers and inline scripts into separate JavaScript files and reference them via the scriptSrc directive.

Beyond the basic directives, CSP offers many more for fine-grained control, such as frameSrc (for <iframe> content), fontSrc (for web fonts), mediaSrc (for audio/video), and even object-src (for plugins like Flash, though these are largely deprecated). You can also specify reporting mechanisms using report-uri or report-to to send violation reports to a specified endpoint, allowing you to monitor and refine your policy over time.

The next step in securing your application is to move away from 'unsafe-inline' and 'unsafe-eval' directives by implementing nonce-based or hash-based CSP.

Want structured learning?

Take the full Express course →