Security implications of cross-origin resource sharing (CORS) in Node.js
Tags: applicationsecurity, codesecurity, javascript, node
Published: 2023-09-14 02:00:22

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)](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy) 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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS) HTTP method to send [preflight requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#preflighted_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)](https://snyk.io/learn/csrf-cross-site-request-forgery/) 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](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) 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](https://expressjs.com/) 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](https://nodejs.org) (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
```

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



```
npm i express cors
```

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}`)
);
```

Express.js exposes [middleware functions](https://expressjs.com/en/guide/writing-middleware.html) 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 module](https://snyk.io/advisor/npm-package/cors)using`const 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) => {
...
} 
```

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) => { /* ... */ });
```

#### 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'];
```

#### 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'];
```

#### 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;
```

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) => { /\* ... \*/ });
```

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](https://snyk.io/product/open-source-security-management/), 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)](https://snyk.io/learn/cross-site-scripting/).


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'));
  }
};
```

#### 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](https://snyk.io/advisor/npm-package/passport) 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) => { /* ... */ });
```

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'];
```

### 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](https://snyk.io/advisor/npm-package/helmet) provides a set of security headers with sensible defaults. However, it requires some [content security policy (CSP)](https://snyk.io/blog/how-can-a-content-security-policy-prevent-xss-and-other-vulnerabilities/) 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
```

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



```
const helmet = require('helmet');
app.use(helmet());
```

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));
```

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](https://snyk.io/product/snyk-code/) and the Snyk developer tool extensions (like the [Visual Studio Code](https://docs.snyk.io/integrations/ide-tools/visual-studio-code-extension) 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](https://www.postman.com/) 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
```

Then, in a web browser of your choice, navigate to the [Google homepage](https://www.google.com).


![](https://res.cloudinary.com/snyk/image/upload/v1694613364/blog-cors-google-home.jpg)
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))
```

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.


![](https://res.cloudinary.com/snyk/image/upload/v1694613364/blog-cors-element-error.jpg)
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) => { /* ... */ })
```

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](https://brave.com/), you will need to [disable Shields](https://support.brave.com/hc/en-us/articles/360022806212-How-do-I-use-Shields-while-browsing-). Please note that the current CORS configuration may fail when using [Safari browser](https://www.apple.com/safari/). This is a result of Safari's strict enforcement of the Same-Origin Policy (SOP), which restricts requests from different origins.


![](https://res.cloudinary.com/snyk/image/upload/v1694613363/blog-cors-google-elements.jpg)
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](https://snyk.io/product/snyk-code/) to test your application in real time and double-check your security settings. It’s free to perform 200 tests per month.