Security implications of cross-origin resource sharing (CORS) in Node.js

SnykSec - Sep 14 '23 - - Dev Community

In modern web applications, cross-origin resource sharing (CORS) enables secure communication between applications hosted on different origins. Developers use CORS to access other applications’ services within their own. This approach eliminates the need to rewrite features from scratch, accelerating development time and improving the developer experience.

As helpful as CORS is, improper implementation can expose your Node.js applications to security risks, such as data breaches and unauthorized access from third-party websites. Misconfigurations may reveal sensitive data to unintended origins or allow malicious websites to bypass same-origin policy (SOP) protections.

Understanding these vulnerabilities and adopting best practices to implement CORS securely helps mitigate these risks but also supports you in maintaining your application’s functionality.

In this article, we’ll first cover what CORS is and some of its use cases. Then, we’ll use a code sample to implement CORS in a Node.js application. After exploring the potential security concerns, we’ll review best practices for using CORS and test our sample’s security. All you’ll need to follow along is some experience with JavaScript and Node.js.

Understanding CORS and its use cases

Web browsers use a security mechanism called same-origin policy (SOP) to regulate how web applications interact. The SOP restricts applications hosted at one origin from reading an application’s resources at a different source. 

This mechanism prevents malicious sites from reading another site's data but can also restrict legitimate uses. What if you want to include weather data in your application? Or embed a YouTube video on a web page? These public resources should be available for everyone to read, but the SOP blocks them.

CORS lets web applications bypass SOP limitations, enabling communication between various web services and allowing cross-origin requests. Applications can use the OPTIONS HTTP method to send preflight requests to the other origin’s resource. Through these preflight requests, web browsers determine if the other origin’s server permits such requests. Based on that information, the browser either disregards or enforces the SOP.

Typical use cases for CORS include loading resources from content delivery networks (CDNs), requesting APIs across multiple domains, and facilitating third-party integration.

However, improperly implementing CORS in your application poses security risks. You may open your app to a cross-site request forgery (CSRF) attack, which tricks users into performing unwanted actions on the application. Plus, overly permissive configurations can allow unauthorized access to user data and sensitive information between origins, potentially causing data breaches or exposing vulnerabilities for bad actors to exploit.

For instance, you may be tempted to use the allow all wildcard, denoted by an asterisk (*), for the Access-Control-Allow-Origin HTTP header value. This CORS configuration permits any origin to interact with your application, opening the door to potential vulnerability. So, instead, opt for specific, restrictive configurations. For dynamic settings of allowed origins, consider leveraging environment variables as a safer alternative.

Securely implementing CORS in a Node.js application

In this section, we’ll walk through a secure implementation of CORS in a Node.js application. We’ll consider a simple Node.js application using Express.js as its web framework. It will serve as an API for a hypothetical online bookstore. 

While the API has multiple endpoints (to fetch, add, update, and delete books), this example will only implement fetching books. For simplicity, the API will interface with a sampleBooksData object that serves as a data storage system.

Prerequisites

To follow this tutorial, you need the following:

  • JavaScript experience
  • Node.js (version 18.16.1 is recommended)
  • A modern web browser

Create the Node.js application

To build the application, first create a new directory for it. Then, in the new directory, run the command below to initialize a new Node.js app:

npm init -y
Enter fullscreen mode Exit fullscreen mode

Next, execute the following command to install the dependencies for this project:

npm i express cors
Enter fullscreen mode Exit fullscreen mode

Finally, create a JavaScript file and name it index.js. Paste in the code below:

// Import required modules
const express = require("express");
const cors = require("cors");

// Initialize the Express application
const app = express();
app.use(express.json());

//Sample object that will serve as a database to store data
let sampleBooksData = {
  books: [
    {
      id: 1,
      title: "Harry Potter",
    },
    {
      id: 2,
      title: "The Da Vinci Code",
    },
    {
      id: 3,
      title: "Twilight",
    },
  ],
};

app.get("/books", cors(), (req, res, next) => {

  try {
    res.status(200).json(sampleBooksData);
  } catch (error) {
    console.error(`Error while reading from DB ${error}`);
    res.status(500).json({ message: "Internal Server Error" });
  }

});

// Start server on port defined by the variable PORT
let port = 3000;
app.listen(port, () =>
  console.log(`Server running at http://localhost:${port}`)
);
Enter fullscreen mode Exit fullscreen mode

Express.js exposes middleware functions to process requests and responses within the app. These functions can intercept and modify incoming requests or outgoing responses, enabling functionality like logging, authentication, or, in our case, configuring CORS. The example above imports the CORS moduleusingconst cors = require("cors");.It applies the module as a middleware function to the /books route using the following code:

app.get("/books", cors(), (req, res, next) => {
...
} 
Enter fullscreen mode Exit fullscreen mode

However, the current implementation of the cors middleware on the /books route doesn’t explicitly specify which origins can make a GET request. So, the setting enables all origins to make requests to the path. This approach isn’t a good idea for security reasons, but you can configure more restrictive rules for your production environment.

Configure CORS

Using four different configurations, you can more securely implement CORS with the cors middleware.

Enable CORS for specific origins

You can set the origin option in your CORS settings to configure allowed origins. This setting enhances security by restricting access to trusted sources and preventing unauthorized cross-origin requests. The example below only allows requests from <https://example.com>:

const corsOptions = {
  origin: 'https://example.com'
};
app.get('/books', cors(corsOptions), (req,res) => { /* ... */ });
Enter fullscreen mode Exit fullscreen mode

Configure allowed HTTP methods

You can also define permitted methods using the methods option. This approach limits potential attack vectors by only permitting necessary actions on your API endpoints. The example below only allows GET and POST actions:

corsOptions.methods = ['GET', 'POST'];
Enter fullscreen mode Exit fullscreen mode

Set custom headers and exposed headers

Another option is configuring the Access-Control-Allow-Headers CORS header using the allowedHeaders option and Access-Control-Expose-Headers with the exposedHeaders option. This approach allows the application to properly handle sensitive data while maintaining flexible communication between client and server components. The example below only allows Content-Type and Authorization headers and only exposes X-Custom-Header:

corsOptions.allowedHeaders = ['Content-Type', 'Authorization'];
corsOptions.exposedHeaders = ['X-Custom-Header'];
Enter fullscreen mode Exit fullscreen mode

Configure preflight requests and caching

Finally, you can enable preflight requests (OPTIONS) caching by setting the cache duration via the maxAge setting. This approach allows the client to cache the server’s CORS policy information for a specified time. When clients make subsequent requests, they can use cached CORS policy data without fetching it again from the server.

Setting an appropriate maxAge value reduces response time and network overhead, optimizing performance while ensuring that CORS policies remain current. The example below sets maxAge to 86400 seconds (24 hours):

// Cache duration in seconds.
// Set maxAge to a high value (e.g., 86400) for long-lived caches.
// Default is no-cache.
const DAY_IN_SECONDS=86400;
corsOptions.maxAge=DAY_IN_SECONDS;
Enter fullscreen mode Exit fullscreen mode

Now that we've configured our options, we can use our code as middleware on any route that requires CORS to be enabled:

app.get('', cors(corsOptions), (req,res) => { /\* ... \*/ });
Enter fullscreen mode Exit fullscreen mode

Although CORS helps secure the API, it is still a third-party dependency. We need to ensure it and any other dependencies we pull into the project are secure, without known vulnerabilities. Snyk Open Source, for example, helps check packages for security concerns during development or in continuous integration/continuous delivery (CI/CD) pipelines.

CORS security implications and best practices

If we don't configure CORS correctly, we risk triggering the security vulnerabilities mentioned earlier. Specifically, we could expose sensitive data to unintended origins, allow unauthorized actions by malicious websites (CSRF attacks), and even enable attackers to bypass SOP protections through techniques such as cross-site scripting (XSS).

To avoid such scenarios, always follow best practices when implementing CORS configurations. You can also take further security steps, like implementing headers outside of CORS, which we'll cover later in this section.

Best practices

Best practices like restricting allowed origins, using secure cookies and tokens, and limiting exposed headers are essential to keep your applications and users secure. We'll cover these below with code samples to show you how to do it.

Restrict allowed origins to an allowlist

To avoid using the allow-all wildcard (*) in the corsOptions object, specify origins in an allowlist:

const allowedOrigins = ['https://example.com', 'https://another-example.com'];
corsOptions.origin = (origin, callback) => {
  if (allowedOrigins.includes(origin)) {
    callback(null, true);
  } else {
    callback(new Error('Not allowed by CORS'));
  }
};
Enter fullscreen mode Exit fullscreen mode

Use secure cookies and tokens for authentication

Use the HTTPS schema to ensure your application transmits cookies and tokens securely. Also, use trusted authentication libraries like Passport.js or JSON Web Token (JWT) solutions:

corsOptions.credentials = true;

app.use(passport.initialize());
passport.use(new JwtStrategy(/* ... */));
app.get('/books', cors(corsOptions), passport.authenticate('jwt', { session: false }), (req,res) => { /* ... */ });
Enter fullscreen mode Exit fullscreen mode

In this code, setting the value of credentials to true allows cross-origin requests to include credentials.

Limit exposed headers and HTTP methods

To specify only essential headers, use the exposedHeaders option. Also, use the methods option in your configuration to restrict which methods enable CORS:

// Exposed Headers.
corsOptions.exposedHeaders = ['Content-Type', 'Authorization'];

// Permitted HTTP Methods
corsOptions.methods=['GET'];
Enter fullscreen mode Exit fullscreen mode

Implement security headers outside of CORS

You can implement additional security headers outside of CORS to further secure your application. HTTP security headers are HTTP headers that indicate the security aspects of an HTTP communication between a client and a server.

Helmet.js provides a set of security headers with sensible defaults. However, it requires some content security policy (CSP) configuration to enable the functionality that the CORS configuration defines. Follow the steps below to set it up.

First, run the following command in your Node.js application directory’s command line to install Helmet:

npm i helmet
Enter fullscreen mode Exit fullscreen mode

Next, import the package into your application using the code below:

const helmet = require('helmet');
app.use(helmet());
Enter fullscreen mode Exit fullscreen mode

Then, configure the CSP to match your CORS settings as below:

const cspConfig = {
  directives: {
    defaultSrc: ["'self'", 'https://example.com'],
    // ...other CSP directives matching your app's needs.
  }
};
app.use(helmet.contentSecurityPolicy(cspConfig));
Enter fullscreen mode Exit fullscreen mode

As a last step, check your work. It’s easy to miss a setting or make a typo that compromises your application’s security, especially as it grows. Snyk Code and the Snyk developer tool extensions (like the Visual Studio Code extension) identify missing or misconfigured security headers in your codebase. Partnering with these third-party tools to proactively address these security issues during development helps maintain a secure Node.js application.

Testing CORS implementations and security

To ensure your application is secure against potential threats, test your CORS implementation and its security. This approach helps identify potential vulnerabilities and misconfigurations. Testing also verifies that the CORS rules are working as intended and not exposing the application to security risks, such as data breaches or unauthorized access from third-party websites.

There are multiple ways to test CORS implementations and security. Below, we’ll cover some popular methods.

Browser developer tools

Modern web browsers offer developer tools to inspect and debug web applications. You can use these tools to test CORS by examining network requests and responses. Simulate cross-origin requests to verify that the CORS rules block or allow them appropriately.

We'll expand on this method later by applying it to the sample Node.js application we built earlier.

Postman

You can use the popular API development and testing tool Postman to test CORS. It enables you to create HTTP requests and specify custom headers to test CORS configurations. Postman can also automatically generate preflight requests to test preflight request caching. Use Postman's scripting capabilities to automate test scenarios, too.

Custom scripts

You can create custom scripts to test CORS configurations. For example, use a scripting language like Python or JavaScript to send HTTP requests and verify that CORS headers are in the responses. You can also simulate cross-origin requests and verify that the CORS rules block or allow the requests appropriately.

How to test CORS configuration using browser developer tools

You can test the CORS configurations in our sample Node.js application using browser developer tools. Follow the steps below.

First, start up the Node.js application. In the application directory’s command line, run the command below:

node index.js
Enter fullscreen mode Exit fullscreen mode

Then, in a web browser of your choice, navigate to the Google homepage.


Next, open the developer console. If you use a Chromium-based browser, use the command Option+⌘+J (on macOS) or Shift+CTRL+ J (on Windows/Linux). Or, use the relevant commands in the browser of your choice.

Paste in the code below, then hit Return/Enter:

fetch('http://127.0.0.1:3000/books')
.then((response) => response.json())
.then((json) => console.log(json))
Enter fullscreen mode Exit fullscreen mode

Based on the sample application’s current CORS configuration, you’ll most likely get errors indicating that the CORS policy blocked the origin (Google) from accessing the /books resource on your server and that there was an internal server error.


The blocked access error is because the origin, ​​https://www.google.com, is not among the origins allowed to overcome the SOP. The settings in the origin option of the corsOptions object we pass to the cors middleware don’t allow that origin to receive a response from the server. Note that although the view and the error message might differ depending on the browser, the error will still indicate that the request is blocked.

This error verifies that our CORS implementation rules are working as intended. The application is not accessible from unauthorized origins.

To approve the request from https://www.google.com, specify it in the corsOptions object’s origin option:

const allowedOrigins = ['https://example.com', 'https://another-example.com', 'https://www.google.com'];
corsOptions.origin = (origin, callback) => {
  if (allowedOrigins.includes(origin)) {
    callback(null, true);
  } else {
    callback(new Error('Not allowed by CORS'));
  }
};

app.get('books/', cor(corsOptions), (req,res) => { /* ... */ })
Enter fullscreen mode Exit fullscreen mode

When you restart the server and rerun the fetch command in the developer console, it should work as expected. If you find you’re still getting the error you may need to try running the script in a different browser. For the fetch command to work on Brave browser, you will need to disable Shields. Please note that the current CORS configuration may fail when using Safari browser. This is a result of Safari's strict enforcement of the Same-Origin Policy (SOP), which restricts requests from different origins.


We’ve allowed https://www.google.com in the CORS settings, so we get a list of books instead of errors.

Next steps

Although CORS adds useful functions to your application, improperly configuring it can expose your app to security vulnerabilities. Given these risks, you should apply best practices whenever possible to fortify your Node.js applications against potential threats.

You can configure the cors middleware with specific origins, HTTP methods, headers, and preflight requests for secure cross-origin communication. Tools like Snyk Code and browser developer tools also help you proactively find errors and vulnerabilities during development. This approach helps ensure your application is secure when it goes to production and keeps your systems and users safe.

Once you have configured your CORS settings by following these best practices, try Snyk Code to test your application in real time and double-check your security settings. It’s free to perform 200 tests per month.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .