Post

Node Production Headache: Security Headers

Node Production Headache: Security Headers

Introduction

Hearing the term “security headers” might be intimidating for someone new to computer science, especially in the field of web development. Security headers are HTTP response headers that provide essential security measures that your application can use to enhance its overall security. These headers play a critical role in ensuring the safety of a website.

The following table outlines some of the most important security headers.

Common Security Headers

Security HeaderExpected ValueUse
Content-Security-Policydefault-src ‘self’;Restricts sources for content to mitigate cross-site scripting (XSS) and data injection.
Cross-Origin-Opener-Policysame-originIsolates browsing contexts to prevent cross-origin information leaks.
Cross-Origin-Resource-Policysame-originLimits the sharing of resources to the same origin.
Origin-Agent-Cluster?1Changes process isolation to be origin-based for improved security.
Referrer-Policyno-referrer or strict-originControls how much referrer information is included with requests.
Strict-Transport-Securitymax-age=31536000; includeSubDomainsEnforces HTTPS for all connections and prevents downgrade attacks.
X-Content-Type-OptionsnosniffPrevents the browser from MIME-sniffing a response away from the declared content-type.
X-DNS-Prefetch-Controlon or offControls DNS prefetching to reduce potential information leaks.
X-Download-OptionsnoopenForces downloads to be saved instead of auto-executed (Internet Explorer only).
X-Frame-OptionsDENY or SAMEORIGINPrevents the webpage from being framed to protect against clickjacking attacks.
X-Permitted-Cross-Domain-Policiesnone or master-onlyControls cross-domain behavior for Adobe products, like Acrobat.
X-Powered-By(Header removed entirely)Prevents revealing server information to mitigate potential attacks.
X-XSS-Protection0Disables buggy XSS filters in older browsers to avoid unintended issues.
Permissions-Policygeolocation=(), microphone=()Restricts access to browser features like geolocation and microphone.
Expect-CTenforce, max-age=86400Ensures that valid Certificate Transparency information is included for HTTPS.
Cross-Origin-Embedder-Policyrequire-corpMitigates cross-origin attacks for resources embedded in the page.
Cache-Controlno-store, no-cache, must-revalidatePrevents sensitive information from being stored in the cache.
Set-CookieSecure; HttpOnly; SameSite=StrictProtects cookies by making them secure, accessible only to HTTP, and tied to the same site.

When building a web application, the developer must set these response headers in the code.

How to Set Security Headers

Let’s consider an example in Node.js. We can set security headers either manually or by using a popular library called ‘helmet’, which acts as middleware to automatically set these headers for you. Using a library like helmet is convenient, so we’ll focus on its implementation. First, we will look at how to use helmet in your code, and then we will discuss the importance of carefully setting security headers when deploying to production.

  • Install Helmet using this command
1
$ npm install helmet
  • Call the helmet middleware in your application’s main file for e.g index.js or app.js
1
2
3
4
5
import helmet from "helmet";

const app = express();

app.use(helmet());

You can learn more about the default headers set by the Helmet.js by reading here.

  • If you want to customize the default headers set by Helmet, you can do so. It is essential to customize the headers because relying solely on the default headers provided by Helmet in a production environment can hinder the functionality of your application. Why is this the case ? Let’s discuss it in the next section.

Security Headers and Production Environment

After coding my Node.js application, I was ready to deploy it in production. I initially thought that the deployment process would go faster since I had already simulated it on my virtual machine, but I was mistaken. If you noticed, I mentioned “headache” in the title of this blog for a reason.

When I simulated the production environment on my virtual machine - it was set up on localhost, also the microsoft login was not set during that time.

When I deployed the application in production on my domain - the following issues were encountered over a span of one week

  1. I encountered an issue where the Microsoft login popup was not appearing. After changing the popup setting to redirect, it directed me to the Microsoft Login page. However, after entering the correct login credentials, I ended up with a blank page.
    • This happened because the Content Security Policy (CSP) was not configured to allow requests from the Microsoft website. The login popup was not appearing because the frameAncestors and frameSrc in the CSP was not configured for “https://login.microsoftonline.com”
    • To resolve this, I updated the connectSrc, frameSrc, frameAncestors in the CSP to include not only the “self” variable but also all the domains from which we expected to receive requests.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        connectSrc: [
          "'self'",
          "https://login.microsoftonline.com",
          "https://graph.microsoft.com",
          "https://mydomain.com",
          "https://somethirdpartydomain.com",
        ],
        frameSrc: [
          "'self'",
          "https://login.microsoftonline.com",
          "https://mydomain.com",
          "blob:",
        ], // for tokens
        frameAncestors: [
          "'self'",
          "https://login.microsoftonline.com",
          "https://mydomain.com",
        ], // Allows embedding in Azure AD frames/popups
      },
    },
  })
);
  1. After the above three changes, I was able to login, but still faced problems. Half of my stylesheet, scripts and images were not present and the pop ups were not showing small elements like a cross svg etc.
    • To resolve that, I updated the scriptSrc, imgSrc and styleSrc in the CSP directives.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        connectSrc: [
          "'self'",
          "https://login.microsoftonline.com",
          "https://graph.microsoft.com",
          "https://mydomain.com",
          "https://somethirdpartydomain.com",
        ],
        frameSrc: [
          "'self'",
          "https://login.microsoftonline.com",
          "https://mydomain.com",
          "blob:",
        ], // for tokens
        frameAncestors: [
          "'self'",
          "https://login.microsoftonline.com",
          "https://mydomain.com",
        ], // Allows embedding in Azure AD frames/popups
        scriptSrc: [
          "'self'",
          "https://login.microsoftonline.com",
          "https://mydomain.com",
        ],
        imgSrc: ["'self'", "blob:", "data:"],
        styleSrc: ["'self'", "'unsafe-inline'", "https://mydomain.com"], // Allows inline styles
      },
    },
  })
);

The following is some of the security header configuration I used to deploy my application to production. Additionally, we need to implement CORS (Cross-Origin Resource Sharing), which is not included in Helmet. CORS can be quite tricky, so I will save that topic for another post. Thank you for reading until the end!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import helmet from "helmet";

const app = express();

app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        connectSrc: [
          "'self'",
          "https://login.microsoftonline.com",
          "https://graph.microsoft.com",
          "https://mydomain.com",
          "https://somethirdpartydomain.com",
        ],
        frameSrc: [
          "'self'",
          "https://login.microsoftonline.com",
          "https://mydomain.com",
          "blob:",
        ], // for tokens
        frameAncestors: [
          "'self'",
          "https://login.microsoftonline.com",
          "https://mydomain.com",
        ], // Allows embedding in Azure AD frames/popups
        scriptSrc: [
          "'self'",
          "https://login.microsoftonline.com",
          "https://mydomain.com",
        ],
        imgSrc: ["'self'", "blob:", "data:"],
        styleSrc: ["'self'", "'unsafe-inline'", "https://mydomain.com"], // Allows inline styles
      },
    },
    crossOriginOpenerPolicy: { policy: "same-origin" },
    crossOriginResourcePolicy: { policy: "same-origin" },
    originAgentCluster: true,
    referrerPolicy: { policy: "no-referrer" },
    hsts: {
      maxAge: 31536000, //  HSTS policy remains in effect for one year
      includeSubDomains: true,
      preload: true,
    },
    noSniff: true,
    dnsPrefetchControl: { allow: false },
    frameguard: { action: "sameorigin" },
    permittedCrossDomainPolicies: { permittedPolicies: "none" },
  })
);
This post is licensed under CC BY 4.0 by the author.