Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions ka_beraoud_esi_dz/limit-overrun-race-condition.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
const express = require('express');
const { db } = require('../utils/db');
const UserRepo = require('../repositories/userRepo');

const app = express();
app.use(express.json());

app.post('/api/purchase', async (req, res) => {
const { userId, itemCost } = req.body;

// ruleid: limit-overrun-race-condition
const user = await db.user.findUnique({ where: { id: userId } });

if (user.balance >= itemCost) {
await db.user.update({
where: { id: userId },
data: { balance: user.balance - itemCost }
});
return res.json({ success: true });
}
return res.status(400).json({ success: false, reason: 'Insufficient balance' });
});

app.post('/api/use-credits', async (req, res) => {
const { userId, cost } = req.body;

// ruleid: limit-overrun-race-condition
const account = await db.account.findOne({ id: userId });

if (account.credits < cost) {
return res.status(400).json({ success: false, reason: 'Insufficient credits' });
}

await db.account.updateOne(
{ id: userId },
{ $set: { credits: account.credits - cost } }
);
return res.json({ success: true });
});

app.post('/api/redeem-reward', async (req, res) => {
const { userId, rewardCost } = req.body;

// ruleid: limit-overrun-race-condition
const user = await UserRepo.findById(userId);

if (user.points >= rewardCost) {
user.points -= rewardCost;
await user.save();
return res.json({ success: true });
}
return res.status(400).json({ success: false, reason: 'Not enough points' });
});

app.post('/api/transfer', async (req, res) => {
const { userId, amount } = req.body;

// ruleid: limit-overrun-race-condition
const user = await db.user.findById(userId);
const { balance } = user;

if (balance > amount) {
await db.user.update({
where: { id: userId },
data: { balance: balance - amount }
});
return res.json({ success: true });
}
return res.status(400).json({ success: false, reason: 'Insufficient funds' });
});

app.post('/api/check-status', async (req, res) => {
const { userId } = req.body;

// ok: limit-overrun-race-condition
const user = await db.user.findUnique({ where: { id: userId } });

if (user.balance >= 1000) {
await db.user.update({
where: { id: userId },
data: { isVIP: true } // Different field mutated
});
}
res.json({ success: true });
});

app.post('/api/purchase-atomic', async (req, res) => {
const { userId, itemCost } = req.body;

// ok: limit-overrun-race-condition
const result = await db.user.updateOne(
{ id: userId, balance: { $gte: itemCost } },
{ $inc: { balance: -itemCost } } // Atomic deduction
);

if (result.modifiedCount > 0) {
return res.json({ success: true });
}
return res.status(400).json({ success: false, reason: 'Insufficient balance' });
});

app.post('/api/activate-user', async (req, res) => {
const { userId } = req.body;

// ok: limit-overrun-race-condition
const user = await UserRepo.findById(userId);

if (user.points > 0) {
user.status = 'active'; // Different field mutated
await user.save();
}
res.json({ success: true });
});

const PORT = process.env.PORT || 3000;
if (require.main === module) {
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}`);
});
}

module.exports = app;
210 changes: 210 additions & 0 deletions ka_beraoud_esi_dz/limit-overrun-race-condition.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
rules:
- id: limit-overrun-race-condition
message: |
Potential TOCTOU limit overrun! A specific property (`$FIELD`) of `$RECORD` is checked and then mutated. Concurrent requests can bypass this check. Use atomic database updates instead (e.g. `UPDATE ... WHERE field > 0`), or use pessimistic locking.
languages:
- javascript
- typescript
severity: ERROR
metadata:
cwe: CWE-367
impact: MEDIUM
likelihood: HIGH
category: security
subcategory:
- vuln
confidence: MEDIUM
owasp:
- A4:2021 Insecure Design
technology:
- express
- nest
references:
- https://portswigger.net/web-security/race-conditions
- https://cwe.mitre.org/data/definitions/367.html
patterns:
- pattern-either:
- patterns:
- pattern-either:
- pattern: |
$RECORD = await $OBJ.$READ(...);
...
if (<... $RECORD.$FIELD ...>) {
...
await $OBJ2.$UPDATE(..., $PAYLOAD, ...);
...
}
- pattern: |
$RECORD = await $OBJ.$READ(...);
...
if (<... $RECORD.$FIELD ...>) { ... return ...; }
...
await $OBJ2.$UPDATE(..., $PAYLOAD, ...);
- pattern: |
$RECORD = await $OBJ.$READ(...);
...
if (<... $RECORD.$FIELD ...>) return ...;
...
await $OBJ2.$UPDATE(..., $PAYLOAD, ...);
- pattern: |
$RECORD = await $OBJ.$READ(...);
...
if (<... $RECORD.$FIELD ...>) { ... throw ...; }
...
await $OBJ2.$UPDATE(..., $PAYLOAD, ...);
- pattern: |
$RECORD = await $OBJ.$READ(...);
...
if (<... $RECORD.$FIELD ...>) throw ...;
...
await $OBJ2.$UPDATE(..., $PAYLOAD, ...);
- pattern: |
$RECORD = await $OBJ.$READ(...);
...
{ ..., $FIELD, ... } = $RECORD;
...
if (<... $FIELD ...>) {
...
await $OBJ2.$UPDATE(..., $PAYLOAD, ...);
...
}
- pattern: |
$RECORD = await $OBJ.$READ(...);
...
$COND = <... $RECORD.$FIELD ...>;
...
if ($COND) {
...
await $OBJ2.$UPDATE(..., $PAYLOAD, ...);
...
}
- pattern: |
$RECORD = await $OBJ.$READ(...);
...
$COND = <... $RECORD.$FIELD ...>;
...
if ($COND) { ... return ...; }
...
await $OBJ2.$UPDATE(..., $PAYLOAD, ...);
- pattern: |
$RECORD = await $OBJ.$READ(...);
...
$COND = <... $RECORD.$FIELD ...>;
...
if ($COND) return ...;
...
await $OBJ2.$UPDATE(..., $PAYLOAD, ...);
- metavariable-pattern:
metavariable: $PAYLOAD
pattern-either:
- pattern: '{ ..., $FIELD: ..., ... }'
- pattern: '{ ..., $WRAPPER: { ..., $FIELD: ..., ... }, ... }'
- patterns:
- pattern-either:
- pattern: |
$RECORD = await $OBJ.$READ(...);
...
if (<... $RECORD.$FIELD ...>) {
...
$MUTATION;
...
$SAVE;
...
}
- pattern: |
$RECORD = await $OBJ.$READ(...);
...
if (<... $RECORD.$FIELD ...>) { ... return ...; }
...
$MUTATION;
...
$SAVE;
- pattern: |
$RECORD = await $OBJ.$READ(...);
...
if (<... $RECORD.$FIELD ...>) return ...;
...
$MUTATION;
...
$SAVE;
- pattern: |
$RECORD = await $OBJ.$READ(...);
...
if (<... $RECORD.$FIELD ...>) { ... throw ...; }
...
$MUTATION;
...
$SAVE;
- pattern: |
$RECORD = await $OBJ.$READ(...);
...
if (<... $RECORD.$FIELD ...>) throw ...;
...
$MUTATION;
...
$SAVE;
- pattern: |
$RECORD = await $OBJ.$READ(...);
...
{ ..., $FIELD, ... } = $RECORD;
...
if (<... $FIELD ...>) {
...
$MUTATION;
...
$SAVE;
...
}
- pattern: |
$RECORD = await $OBJ.$READ(...);
...
$COND = <... $RECORD.$FIELD ...>;
...
if ($COND) {
...
$MUTATION;
...
$SAVE;
...
}
- pattern: |
$RECORD = await $OBJ.$READ(...);
...
$COND = <... $RECORD.$FIELD ...>;
...
if ($COND) { ... return ...; }
...
$MUTATION;
...
$SAVE;
- pattern: |
$RECORD = await $OBJ.$READ(...);
...
$COND = <... $RECORD.$FIELD ...>;
...
if ($COND) return ...;
...
$MUTATION;
...
$SAVE;
- metavariable-pattern:
metavariable: $MUTATION
pattern-either:
- pattern: $RECORD.$FIELD = ...
- pattern: $RECORD.$FIELD += ...
- pattern: $RECORD.$FIELD -= ...
- pattern: $RECORD.$FIELD++
- pattern: $RECORD.$FIELD--
- pattern: ++$RECORD.$FIELD
- pattern: --$RECORD.$FIELD
- metavariable-pattern:
metavariable: $SAVE
pattern-either:
- pattern: await $RECORD.$UPDATE(...)
- pattern: await $OBJ3.$UPDATE($RECORD, ...)
- metavariable-regex:
metavariable: $READ
regex: ^(findOne|findUnique|findFirst|findById|findByPk|find|findOneBy)$
- metavariable-regex:
metavariable: $UPDATE
regex: ^(update|updateOne|updateMany|save|findByIdAndUpdate|findOneAndUpdate)$
Loading