Race condition - TypeScript

Race condition - TypeScript

Need

Prevention of race conditions in the system

Context

  • Usage of TypeScript for statically typed JavaScript development
  • Usage of Express for building web applications and APIs

Description

Non compliant code

        import express from 'express';

const app = express();

let data = '';

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

app.post('/api/data', (req, res) => {
  const newData = req.body.data;
  data = newData;
  res.send('Data updated successfully');
});

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

The vulnerability in this code is a race condition.

A race condition occurs when multiple threads or processes access shared data concurrently, and the result of the execution depends on the order in which the threads/processes are scheduled. In this case, the vulnerability arises because the data variable is accessed and modified by multiple requests simultaneously.

In the code, the data variable is a shared resource that can be accessed and modified by both the GET and POST routes. The GET route returns the current value of data, while the POST route updates the value of data based on the request body.

If multiple requests are made to the POST route simultaneously, there is a possibility that the value of data can be overwritten or modified incorrectly. For example, if two requests are made to update data at the same time, the value set by one request may be overwritten by the other request, leading to unexpected behavior.

This vulnerability can have several impacts. An attacker could potentially exploit this race condition to overwrite, delete, or read arbitrary files from the system. Additionally, it can cause unexpected behavior in the application, leading to data corruption or inconsistent states.

To mitigate this vulnerability, it is recommended to ensure that the verification and use of inputs are performed atomically or in a synchronized manner. This can be achieved by using locks, mutexes, or other synchronization mechanisms to ensure that only one request can modify the data variable at a time.

Steps

  • Implement a locking mechanism to ensure that the data variable is accessed and modified atomically.
  • Use a mutex or a semaphore to control access to the data variable.
  • Before modifying the data variable, acquire the lock and release it after the modification is complete.
  • Ensure that the verification and use of inputs are done within the critical section protected by the lock.
  • Consider using a database or a file system to store the data instead of a global variable to avoid race conditions.

Compliant code

        import express from 'express';
import { Mutex } from 'async-mutex';

const app = express();
const mutex = new Mutex();
let data = '';

app.get('/api/data', (req, res) => {
  mutex.acquire().then((release) => {
    try {
      res.send(data);
    } finally {
      release();
    }
  });
});

app.post('/api/data', (req, res) => {
  const newData = req.body.data;
  mutex.acquire().then((release) => {
    try {
      data = newData;
      res.send('Data updated successfully');
    } finally {
      release();
    }
  });
});

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

The fixed code addresses the race condition vulnerability by implementing a mutual exclusion mechanism using the async-mutex library.

First, the code imports the necessary dependencies, including the express framework and the Mutex class from the async-mutex library.

Then, an instance of the express application is created, and a new mutex object is instantiated using the Mutex class.

A global variable data is declared to store the data that will be accessed and modified by the routes.

The code defines two routes: a GET route at '/api/data' and a POST route at '/api/data'.

In the GET route, the mutex is acquired using the acquire() method, which returns a promise. Once the mutex is acquired, the callback function is executed. Inside the callback, the response is sent with the current value of data. Finally, the mutex is released using the release() method.

In the POST route, the request body is extracted to get the new data. Similar to the GET route, the mutex is acquired, and the callback function is executed. Inside the callback, the data variable is updated with the new value, and a success message is sent in the response. Finally, the mutex is released.

By using the mutex, only one request can acquire the lock at a time, ensuring that the critical sections of code (accessing and modifying data) are executed atomically. This prevents multiple requests from interfering with each other and avoids the race condition vulnerability.

The application is then started and listens on port 3000, with a log message indicating that the server has started.

References