<Saragam />

Handling Cross-Origin Resource Sharing

June 8, 2024

Cross-Origin Resource Sharing (CORS) is a security mechanism implemented by web browsers to restrict access to resources from a different domain. It is designed to prevent malicious attacks such as cross-site scripting (XSS) and cross-site request forgery (CSRF).It allows a web server to specify which domains are allowed to access its resources. When a client makes a request to a server, the server includes a special header called Access-Control-Allow-Origin in its response. This header specifies which domain is allowed to access the resources. If the requesting domain is not in the allowed list, the browser will block the request.

Implementing CORS with Node.js

Let's assume we have a Node.js server with an endpoint that serves blog post data at http://localhost:5000. We want to allow only requests from http://localhost:3000 to access this endpoint.

\*_Note: Throughout this document localhost is used for baseUrl of the server and frontend application. Feel free to change the baseUrl and ports as per your requirement and real domain in development and production._

To enable CORS in Node.js application, we can use the cors middleware package in our Node.js server as follows:

const express = require("express");
const cors = require("cors");
const app = express();

const whitelist = ["http://localhost:3000"]; // assuming front-end application is running on localhost port 3000

const corsOptions = {
  origin: function (origin, callback) {
    if (whitelist.indexOf(origin) !== -1) {
      callback(null, true);
    } else {
      callback(new Error("Not allowed by CORS"));
    }
  },
};

app.use(cors(corsOptions));

// API routes

app.get("/api/booklist", (req, res) => {
  // return booklist data
});

app.listen(5000, () => {
  console.log("Server running on port 5000");
});

Here, we have defined a whitelist of allowed domains(one or more) and created a corsOptions object that defines the origin callback function. The origin function checks if the requesting domain is in the whitelist and calls the callback function with either true or an error accordingly. We then pass the corsOptions object to the cors middleware using the app.use() function. This will apply the CORS configuration to all requests to our server.

In the example code, the corsOptions object allows requests only from "http://localhost:3000". If the Origin header of the incoming request does not match, the request will be rejected with a "Not allowed by CORS error".

Implementing CORS with Axios

Now, let's assume we have a client-side application "http://localhost:3000" that needs to make requests to our server using Axios. We can configure Axios to send the Origin header with every request, so that our Node.js server can recognize the our domain.

Remember, if the client sends a request to the server, the Origin header will be added automatically by the browser, indicating the Origin of system that initiated the request. Server can retrieve the Origin header from the req.headers.origin property. Therefore, even if we do not manually set the Origin header in the Axios instance, Node.js server will still receive the Origin header in the request headers, provided that the browser includes it in the request.

In fact, the Origin header is a restricted header, which means that it cannot be modified by JavaScript code running in the browser. This is a security feature that prevents attackers from forging the Origin header to bypass cross-origin resource sharing (CORS) restrictions.

Here's how to create a Axios instance to fetch the book list from the API created above:

import axios from "axios";

const BASE_URL = "http://localhost:3000/api";

const axiosInstance = axios.create({
  baseURL: BASE_URL,
});

// ...using axiosInstance to make requests

axiosInstance
  .get("/booklist")
  .then((response) => {
    console.log(response.data);
  })
  .catch((error) => {
    console.error(error);
  });

In this example, the Axios instance is created with the base URL of the server, and a GET request is made to an API endpoint.

Here is another example of a single page application that uses Axios to fetch data from a server and display it in the page. The code loads the Axios library from a CDN using a script tag, and then makes a GET request to an API endpoint on the server to fetch a list of books.

<!DOCTYPE html>

<html>
  <head>
    <title>Book List</title>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  </head>

  <body>
    <h1>My Book List</h1>
    <ul id="book-list"></ul>

    <script>
      const bookList = document.getElementById("book-list");

      axios
        .get("http://localhost:5000/api/booklist")
        .then((response) => {

          const posts = response.data;

          for (let book of books) {
            const li = document.createElement("li");
            li.innerText = post.title;
            bookList.appendChild(li);
          }
        })
        .catch((error) => {
          console.log(error);
        });
    </script>
  </body>
</html>

Monolithic Vs Microservices Architecture

The explanations of origin, CORS, and origin validation provided above are relevant to Microservices Architecture. In a Microservices architecture, an application is broken down into smaller, independent services that communicate with each other through APIs. Each service can be developed, deployed, and scaled independently. For example, the React front-end and Node.js back-end can be separated into different services, each with its own codebase and deployment pipeline. Each service may have its own domain, and requests from one service to another is considered cross-origin.

In contrast, a monolithic architecture couples the front-end and back-end together, and deploys the application as a single unit and domain. In this design, the Node.js server serves both the static front-end files and the back-end API endpoints. Requests from the front-end to the back-end are made to the same domain, and the same-origin policy is enforced by default.

Handling non-CORS in Monolithic Architecture

When a request is made without an Origin header, it is considered a non-CORS request. This happens in Monolithic design, as requests are made from local files rather than a different application. In this case the req.headers.origin property is null/undefined. This means, server cannot determine whether the request is allowed or not based on the Origin header and the whitelist as in the previous example for Microservice design.

Here's how to ensure that the server can properly control access to its resources and prevent unauthorized requests in a Monolithic Architecture:

  • Save the static files in a folder on Node.js/Express.js server. For example, create a folder called "public" and copy static files along with index.html file there. You can use React to build static files and index.html but in this example, simple HTML front-end is used with JavaScript.

  • In your Express.js application, create a route to serve the index.html file when a user navigates to "http://localhost:5000".

  • Start your Node.js application by running node index.js (assuming server code is in index.js). Navigate to "http://localhost:5000" in the web browser to view the website.

Front-end: index.html

<!DOCTYPE html>

<html>
  <head>
    <title>Book List</title>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
  </head>

  <body>
    <h1>My Book List</h1>
    <ul id="book-list"></ul>

    <script>
      const bookList = document.getElementById("book-list");

      axios
        .get("http://localhost:5000/api/booklist")
        .then((response) => {

          const posts = response.data;

          for (let book of books) {
            const li = document.createElement("li");
            li.innerText = post.title;
            bookList.appendChild(li);
          }
        })
        .catch((error) => {
          console.log(error);
        });
    </script>
  </body>
</html>

Back-end Server: index.js

const express = require('express');

const app = express();
const corsOptions = {
  origin: (origin, callback) => {
    if (!origin || origin === 'http://localhost:5000') {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },

  optionsSuccessStatus: 200;
}

app.use(cors(corsOptions));

// Serve static files from the 'public' folder

app.use(express.static('public'));

// your API routes go here

app.get('/api/booklist', (req, res) => {
  // return your booklist data
});

// Route to serve the index.html file

app.get('/', (req, res) => {
    res.sendFile(__dirname + '/public/index.html');
});

// Start the server

app.listen(3000, () => {
    console.log('Server is listening on port 3000');
});

In this example, we have created a route to serve the index.html file when a user navigates to the root URL (/). And also a Api route("/api/booklist") to serve a Axios get request from front end. We have also used the express.static middleware to serve any static files from the public folder. Finally, we have started the server and logged a message to the console.

Drawback of Monolithic architecture

When a request is made from tools like Postman, Thunderclient or directly from the browser's address bar, the "Origin" header may not be sent. In such cases, the "Origin" header would be undefined and the condition "if (!origin || origin === 'http://localhost:5000')" would allow such request to go through.

Whether or not this is a threat to the application depends on the specific use case and security requirements. It may not be a significant threat if you think, when a request is sent from a proper application, the Origin header will be present, and the server can properly check it anyway. But only if the server can properly check the header or requests sent from Postman, Thunderclient or directly from the browser's address bar are only from trusted sources.

To ensure the security of your monolithic design, it's important to recognize that the conditional check "if (!origin || origin === 'http://localhost:5000')" can be vulnerable. This can enable untrusted sources to send requests to the server. So it's important to handle undefined or missing headers gracefully and to validate and sanitize any input received from the client-side. It's also worth noting that an attacker could potentially craft a request with a fake "Origin" header to bypass certain security checks even without a proper front-end application. While filtering the "Origin" header can provide some basic protection against CSRF attacks, it should not be the sole method of request validation.

Therefore, it's important to use additional authentication and authorization methods to strengthen the request validation process. Implementing user authentication and role-based authorization can help to ensure that requests are coming from authenticated users who are authorized to perform specific actions. Techniques like password hashing, CSRF tokens, input validation and token-based authentication can also be used to prevent unauthorized access to the system and ensure that requests are coming from legitimate users/system.

Back to blog
;