Full Report
Web applications can handle multiple requests simultaneously. Because of this, it's important to consider what happens when your code has multiple users at the same time - aka concurrency. In the case of databases, this is a huge deal. You can query a database to verify information. However, this data can quickly become stale and make the security check irrelevant if you're not careful. This is because of the concurrency of these applications. Transactions define a logical unit of work within the database context. These consist of multiple database operations that must be executed simultaneously and successfully for the transaction to succeed. Isolation defines the level at which concurrent transactions will be isolated from each other. These are to used to prevent dirty reads, phantom reads, and non-repeatable reads because of modification. All of these can cause havoc on an application. At the level of Read Uncommitted, all data, including uncommitted data, can be read. At Read Committed, it only reads fully committed data, preventing dirty reads from the last step. This is the default setting for all DBs besides MySQL. The next level is Repeatable Read. In the previous setting, transactions that are finalized can affect information within an ongoing transaction. In this setting, all transaction changes that occur during the transaction are effectively ignored during its execution for individual rows being operated on. The final setting is Serializable. This prevents Phantom reads, which are when different data is read between queries. This requires locking an entire index. Most vulnerabilities that occur from improper database locking settings appear as Time of Check vs. Time of Use (TOCTOU) issues. They use a bank transfer as an example. The first destructive pattern they call out is Calculations Using Current Database State. This is where a query is made to the DB and validation is performed. However, the information in the query doesn't consider the other transactions being executed. In the case of a bank transfer, this could allow two transfers of $100, even though your account only has $100 total. The first update puts it to zero, while the second puts it to -$100. The next pattern is Calculations Using Stale Values. This happens when the code reads the current state of an entry, performs calculations, and then calls UPDATE based upon this. In the case of a bank transfer, this leads to multiple operations appearing as a single one. The value subtracted is $100 that should be done twice on the value. Instead, it's done a single time because of the values that the update has access to. Given the complexity of current applications, they were uncertain about the viability of this attack. So, they set up an application in AWS Fargate with a chosen database in either Golang or Node. After running the attack described above on a bank-transfer-like endpoint, they were able to hit the attack on all settings except when the Serializable level was used. Pretty neat! How do we mitigate this? Conceptually, critical sections should be put at the beginning of a transaction to ensure database entry isolation. In practice, the easiest thing to do is add Serializable transaction isolation level to these transactions. This would have a large impact on the application though. Another option is to add a MUTEX via FOR SHARE or FOR UPDATE on SELECT operations. This will instruct the database to wait until the transaction is complete, allowing for the reading/editing of these fields. A final way is to add a version row to each column. By comparing the version on the read vs. the write, it will prevent race conditions. Overall, a great post on exploiting TOCTOU issues pertaining to databases. I particularly enjoyed the mitigations section of it, as this is a tough issue to fix.
Analysis Summary
# Vulnerability: Database Transaction Race Conditions (TOCTOU)
## CVE Details
- **CVE ID**: N/A (General architectural weakness)
- **CVSS Score**: Estimated 7.4 (High) - [CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:H]
- **CWE**: CWE-367: Time-of-Check Time-of-Use (TOCTOU) Race Condition; CWE-401: Concurrency Issues
## Affected Systems
- **Products**: Web applications utilizing relational databases (RDBMS).
- **Versions**: Platform independent; affects applications written in Go, Node.js, Python, etc.
- **Configurations**: Databases using default isolation levels:
- **PostgreSQL**: Read Committed (Default)
- **MySQL/MariaDB**: Repeatable Read (Default)
- **Read Uncommitted** (Any DB)
## Vulnerability Description
The vulnerability arises from improper concurrency control within database transactions. When an application performs a "Check" (e.g., verifying a bank balance) and later performs a "Use" (e.g., updating the balance), a race condition can occur if multiple requests execute simultaneously.
Two primary patterns were identified:
1. **Calculations Using Current Database State**: Validation is performed against a snapshot that ignores other concurrent inflight transactions, leading to integrity failures (e.g., double-spending).
2. **Calculations Using Stale Values**: The application reads a value, modifies it in memory, and writes it back. Concurrent updates are overwritten because the `UPDATE` is based on the initial stale read, causing "Lost Updates."
## Exploitation
- **Status**: PoC available (Doyensec Research)
- **Complexity**: Medium (Requires timing concurrent requests)
- **Attack Vector**: Network
## Impact
- **Confidentiality**: None
- **Integrity**: Total (Data corruption, unauthorized balance transfers, bypassing business logic)
- **Availability**: High (Database state inconsistency can crash application logic)
## Remediation
### Patches
As this is a logical flaw in application code/database interaction, no vendor patch exists. Developers must refactor transaction handling.
### Workarounds/Mitigations
- **Isolation Level Elevation**: Set the transaction isolation level to `SERIALIZABLE`. This is the most secure method but incurs a significant performance penalty.
- **Atomic Database Operations**: Use `UPDATE` statements that perform calculations directly in SQL (e.g., `SET balance = balance - 100`) rather than in application code.
- **Pessimistic Locking**: Use `SELECT ... FOR UPDATE` or `SELECT ... FOR SHARE`. This creates a Mutex (lock) on the specific rows, forcing other transactions to wait.
- **Optimistic Locking**: Implement a versioning column for each row. The `UPDATE` should only succeed if the version matches the one read at the start of the transaction.
## Detection
- **Indicators of Compromise**: Unexpected negative values in unsigned fields, duplicate transaction IDs, or "Lost Update" symptoms where only one of several concurrent actions is recorded.
- **Detection Methods**:
- **Static Analysis**: Audit code for `SELECT` followed by `UPDATE` patterns within transactions without internal locking.
- **Dynamic Testing**: Use concurrency testing tools (like the Go script provided in the research) to send bursts of simultaneous requests to sensitive endpoints.
## References
- Doyensec Research: hxxps://blog.doyensec.com/2024/07/11/database-transactions-race-conditions.html
- Doyensec Playground (PoC): hxxps://github[.]com/doyensec/db-race-conditions-playground