logo

Race condition - Typescript


Need

Prevention of race conditions in the system


Context

  1. Usage of TypeScript for statically typed JavaScript development
  2. Usage of Express for building web applications and APIs

Description

Insecure Code Example

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

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

Secure Code Example

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

  • 124 - Race condition

  • Last updated

    2023/09/18