Security implications of cross-origin resource sharing (CORS) in Node.js
2023年9月13日
0 分で読めます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
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:
1// Import required modules
2const express = require("express");
3const cors = require("cors");
4
5// Initialize the Express application
6const app = express();
7app.use(express.json());
8
9//Sample object that will serve as a database to store data
10let sampleBooksData = {
11 books: [
12 {
13 id: 1,
14 title: "Harry Potter",
15 },
16 {
17 id: 2,
18 title: "The Da Vinci Code",
19 },
20 {
21 id: 3,
22 title: "Twilight",
23 },
24 ],
25};
26
27app.get("/books", cors(), (req, res, next) => {
28
29 try {
30 res.status(200).json(sampleBooksData);
31 } catch (error) {
32 console.error(`Error while reading from DB ${error}`);
33 res.status(500).json({ message: "Internal Server Error" });
34 }
35
36});
37
38// Start server on port defined by the variable PORT
39let port = 3000;
40app.listen(port, () =>
41 console.log(`Server running at http://localhost:${port}`)
42);
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 module 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`:
1const corsOptions = {
2 origin: 'https://example.com'
3};
4app.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('<route-of-your-choice>', 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, 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:
1const allowedOrigins = ['https://example.com', 'https://another-example.com'];
2corsOptions.origin = (origin, callback) => {
3 if (allowedOrigins.includes(origin)) {
4 callback(null, true);
5 } else {
6 callback(new Error('Not allowed by CORS'));
7 }
8};
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:
1corsOptions.credentials = true;
2
3app.use(passport.initialize());
4passport.use(new JwtStrategy(/* ... */));
5app.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:
1// Exposed Headers.
2corsOptions.exposedHeaders = ['Content-Type', 'Authorization'];
3
4// Permitted HTTP Methods
5corsOptions.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 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
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:
1const cspConfig = {
2 directives: {
3 defaultSrc: ["'self'", 'https://example.com'],
4 // ...other CSP directives matching your app's needs.
5 }
6};
7app.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 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
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:
1fetch('http://127.0.0.1:3000/books')
2.then((response) => response.json())
3.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.
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:
1const allowedOrigins = ['https://example.com', 'https://another-example.com', 'https://www.google.com'];
2corsOptions.origin = (origin, callback) => {
3 if (allowedOrigins.includes(origin)) {
4 callback(null, true);
5 } else {
6 callback(new Error('Not allowed by CORS'));
7 }
8};
9
10app.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, 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.