Race Condition in Feathersjs with Basic Auth
Race Condition in Feathersjs with Basic Auth — how this specific combination creates or exposes the vulnerability
A race condition in a Feathersjs service using HTTP Basic Authentication can occur when authorization checks and state-modifying actions are not performed as a single, atomic operation. Because Basic Auth sends credentials on every request, an attacker can exploit timing differences between validation and execution to bypass intended access controls.
Consider a Feathersjs service that manages a shared numeric balance. The intended logic is: authenticate with Basic Auth, then ensure the requesting user owns the resource before allowing a withdrawal. An insecure implementation might look like this:
// services/balance/balance.service.js
const { Service } = require('feathersjs');
class BalanceService extends Service {
async create(data, params) {
const { user } = params;
// Vulnerable: reads current balance in one step
const current = await this.Model.findOne({ where: { userId: user.id } });
if (!current) throw new Error('Not found');
return current;
}
async update(id, data, params) {
const { user } = params;
// Vulnerable: authorization check and write are separate steps
const current = await this.Model.findOne({ where: { userId: user.id, id } });
if (!current) throw new Error('Unauthorized');
// Time-of-check to time-of-use (TOCTOU): state may change here
return this.Model.updateOne({ id, $set: { balance: data.amount } });
}
}
module.exports = function () {
this.use('/balance', new BalanceService({ Model: this.app.get('BalanceModel') }));
};
An authenticated user Alice (with valid Basic Auth credentials) calls GET /balance to read her balance. Between that read and the subsequent update, an attacker who has compromised or replayed Alice’s credentials could modify the balance. Because the service does not lock or atomically verify-and-apply, the second request may operate on stale data, leading to lost updates or unauthorized changes. This is a classic Time-of-Check-to-Time-of-Use (TOCTOU) race condition.
The risk is elevated in distributed deployments where multiple instances share the same database but do not coordinate state at the point of update. Basic Auth ensures the request presents credentials, but it does not bind the session to a specific execution context; without a transaction or optimistic locking, the window for interference remains open. The scanner checks for such patterns under BOLA/IDOR and Unsafe Consumption categories, flagging missing atomicity as a high-severity finding.
Basic Auth-Specific Remediation in Feathersjs — concrete code fixes
Remediation focuses on making authorization checks and state changes indivisible. Use database transactions or optimistic locking so that the read-validate-write sequence is executed as a single unit. Below is a secure version of the balance service using a transaction-like pattern with an optimistic concurrency field (e.g., version).
// services/balance/balance.service.js
const { Service } = require('feathersjs');
class BalanceService extends Service {
async update(id, data, params) {
const { user } = params;
const { balance, version } = data;
// Execute within a transaction if your database supports it
return this.app.get('knex').transaction(async trx => {
const row = await trx(this.Model.tableName)
.where({ id, userId: user.id, version })
.first()
.forUpdate(); // Pessimistic lock where available
if (!row) throw new Error('Unauthorized or stale');
const [updated] = await trx(this.Model.tableName)
.where({ id, version })
.update({ balance, version: version + 1 })
.returning('*');
return updated;
});
}
}
module.exports = function () {
this.use('/balance', new BalanceService({ Model: this.app.get('BalanceModel'), knex: this.app.get('knex') }));
};
If your storage layer does not support transactions, implement an atomic compare-and-swap using a version or timestamp column. The key is to include the version in both the read and the write condition so that concurrent modifications fail safely. The scanner’s BFLA/Privilege Escalation and Property Authorization checks will validate that such controls are present.
Additionally, ensure that Basic Auth is always served over HTTPS to prevent credential interception. In Feathers, you can enforce this at the adapter or hook level, but the protocol itself must be protected externally. Combine transport-layer enforcement with atomic server-side checks to eliminate the race window.