Skip to content

[Bug] public-api: updateSingleData quota check is susceptible to TOCTOU race — concurrent updates can collectively exceed the database limit #260

Description

@anshul23102

Describe the bug

In apps/public-api/src/controllers/data.controller.js, the updateSingleData handler reads project.databaseUsed from the request-time project snapshot and checks the quota before writing:

const existingDoc = await Model.findOne(queryFilter).lean();
const oldSize = mongoose.mongo.BSON.calculateObjectSize(existingDoc);
const newSize = mongoose.mongo.BSON.calculateObjectSize(simulatedNewDoc);
const sizeDelta = newSize - oldSize;

if (sizeDelta > 0) {
    if ((project.databaseUsed || 0) + sizeDelta > project.databaseLimit) {
        return next(new AppError(403, 'Storage quota exceeded.'));
    }
}

result = await Model.findOneAndUpdate(queryFilter, ...);
await Project.findByIdAndUpdate(project._id, { $inc: { databaseUsed: sizeDelta } });

The project.databaseUsed value is fetched once at request start. Multiple concurrent update requests can all read the same stale value, all pass the quota check, and all proceed to write and increment the counter, collectively pushing the database usage significantly over the declared limit.

To Reproduce

  1. Set a project database limit close to its current usage (e.g., 1 MB used, 1.1 MB limit).
  2. Send 10 concurrent PATCH requests, each adding ~50 KB of data to a document.
  3. Each request reads the same databaseUsed = 1 MB and passes the check (1 MB + 50 KB < 1.1 MB).
  4. All 10 updates succeed, pushing actual usage to ~1.5 MB — 400 KB over the limit.

Expected behavior

The size check and the increment should be atomic. Use a conditional MongoDB update that enforces the limit at the database level:

const result = await Project.findOneAndUpdate(
    { _id: project._id, $expr: { $lte: [{ $add: ['$databaseUsed', sizeDelta] }, project.databaseLimit] } },
    { $inc: { databaseUsed: sizeDelta } }
);
if (!result) return next(new AppError(403, 'Storage quota exceeded.'));

Then proceed with the document update only after the quota increment succeeds.

Additional context

File: apps/public-api/src/controllers/data.controller.js, function: updateSingleData

This pattern is already used correctly in insertData (single-document insert). updateSingleData should apply the same approach.

Labels: bug, nsoc26, level3

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions