Helpful first: Database integrity and what answers reveal
Thirty days to Halcyon · Case 11: Prove it · Episode 9 of 9
The auditors are in the building. Kael Cho, Staff Infrastructure Engineer, turns his screen toward you. A test tenant has paid for one burn's worth of fuel. He sends two bookings in the same instant. Both succeed.
"Each one checked the quota," he says. "Each one was right."
Vera Vance, your mentor, reads the timestamps, then looks at you. "Both checks passed. So where exactly did the fuel go missing?"
By the end you can
- Trace a time-of-check to time-of-use race and name the fix that closes it
- Explain why parameterized queries end SQL injection at its root
- Match each classic application flaw to its root cause and its fix
Two bookings, one fuel quota
The maneuver-booking API handles a booking in two steps. First it checks that the tenant has enough fuel quota. Then it debits the quota. Each step is correct. The trouble is the gap between them.
Kael's two requests arrive together. The first one checks the quota and finds enough. Before it debits, the second one checks too, and it also finds enough. Then both debit. The tenant has reserved twice the fuel it paid for.
This flaw has a name. A race condition is a bug whose result depends on the timing of events. Time-of-check to time-of-use (TOCTOU) is one kind of race condition. The system checks a security precondition at one moment, T1. It uses the resource at a later moment, T2. An attacker acts in the window between T1 and T2.
A concurrent request is one way to use that window. Another is to swap a file's symlink after the system has verified the file's ownership. In both cases the check was true when it ran. It was no longer true when the system acted on it.
- Booking A checksQuota is enough
- Booking B checksQuota is still enough
- Booking A debitsQuota is now used up
- Booking B debitsQuota is spent a second time
SPOT THE PROBLEM
This is the booking log for Kael's test. The tenant has quota for one burn. Flag the lines that show the race.
Show the answers
- 14:02:07.105 booking B quota check ok remaining=1 Booking A has checked but not yet debited. This check reads a quota that booking A is about to spend.
- 14:02:07.110 booking B debit ok remaining=-1 Booking B acts on a check that is no longer true. The quota goes below zero.
Make the check and the action one step
You cannot fix this race by making the check better. The check was already right. The flaw is that the check and the action are separate steps, and something can happen between them.
So the fix is to remove the gap. The check and the action must become a single atomic operation. Atomic means nothing else can run in the middle. Two tools provide that. One is an exclusive lock, which makes every other request wait. The other is a serializable database transaction, which runs as if no other transaction were running beside it.
Vera uses both on the booking API. She wraps the fuel check and the debit inside one serializable transaction with row locking. Booking A locks the tenant's quota row, checks it, debits it, and commits. Booking B waits. When it runs its own check, the quota is gone, and it is refused.
PUT IT IN ORDER
Vera's fix is live. Kael sends bookings A and B together again. Put the events in the order they now happen.
- Booking B asks for the same row and has to wait
- Booking A checks the quota, debits it, and commits
- Booking B gets the row and checks the quota
- Booking B finds no quota left and is refused
- Booking A opens a transaction and locks the tenant's quota row
Show the answer
- Booking A opens a transaction and locks the tenant's quota row
- Booking B asks for the same row and has to wait
- Booking A checks the quota, debits it, and commits
- Booking B gets the row and checks the quota
- Booking B finds no quota left and is refused
The lock makes the check and the debit one step. Booking B cannot check until booking A has finished, so it reads the quota as it really is.
When input becomes part of the command
Kael's second bug is in the booking search. The endpoint takes a filter string from the user. It joins that string onto a PostgreSQL query and sends the result to the database.
That is an injection vulnerability. Injection arises whenever untrusted data is concatenated into a command or query that a parser will read. The parser cannot tell which characters the developer wrote and which the user sent. A single quote or a semicolon in the filter can change the logic of the query. This form is called SQL injection.
The fix is the parameterized query, also called a prepared statement. The database compiles the SQL syntax tree first. Then it binds the user's parameters as plain values. The shape of the query is fixed before any input arrives, so no character in the input can alter it.
Input validation helps, and so does a web application firewall (WAF). Both are worth having as extra layers. Neither is the root fix, because the query is still built by joining strings. Vera replaces the concatenation with a prepared statement. She also adds a static application security testing (SAST) rule that blocks unparameterized SQL in continuous integration (CI).
Concatenated string
- Input is joined onto the query text
- The parser reads code and input together
- A quote or semicolon can alter the logic
Parameterized query
- The syntax tree is compiled first
- Input is bound afterward as a value
- Input stays data, whatever it contains
YOUR CALL
Forty minutes before the auditors test search
The auditors will test the booking search endpoint at 15:00. It is 14:20. Kael asks you which fix to ship.
Kael has three patches ready. Which one do you ask him to ship?
The fix is in. Sloane asks how Meridian knows this bug will not come back in another service.
Strong calls fix the place where input meets the parser, then make the pipeline enforce the fix so it does not depend on memory.
Show the strong calls
- Rewrite the query as a prepared statement with a bound parameter. The auditor sends a filter full of quotes and semicolons. The search returns no rows and nothing else happens. The finding is closed at its cause.
- Add a pipeline rule that blocks any query built without parameters. Two days later the rule stops a merge in another service. The developer fixes it in ten minutes. Sloane files the blocked build as evidence.
The rest of the list follows the same pattern
The race and the injection share a pattern. Each has one root cause, and the lasting fix sits at that cause. The other classic flaws follow it too.
Cross-site scripting (XSS) happens when untrusted data is rendered in the browser's page without escaping. The fix is context-aware output encoding, which encodes data for the exact place it lands in the page. A Content Security Policy (CSP) backs that up.
Cross-site request forgery (CSRF) uses the browser's own habit. The browser attaches session cookies automatically, even to a state-changing request that came from another origin. The fix is an anti-CSRF token on each such request, along with the SameSite cookie attribute. Server-side request forgery (SSRF) sits on the same list. As the name says, the forged request comes from the server instead of the browser.
A buffer overflow is a memory flaw. A program writes past the bounds of the memory it allocated. The extra bytes overwrite adjacent pointers on the stack or the heap. Memory-safe languages and bounds checking remove the cause. Two memory protections add to them, and the table names both.
| Flaw | Root cause | Root fix |
|---|---|---|
| SQL or command injection | Untrusted input concatenated into an interpreter's command string | Parameterized queries (prepared statements) and strict APIs |
| Cross-site scripting (XSS) | Untrusted data rendered in the browser DOM without escaping | Context-aware output encoding and a Content Security Policy (CSP) |
| Cross-site request forgery (CSRF) | The browser automatically attaches ambient session cookies to cross-origin, state-changing requests | Anti-CSRF cryptographic tokens and SameSite cookie attributes |
| TOCTOU race condition | State changes between a separate authorization check and the use of the resource | Atomic check-and-use operations, with mutex or transactional row locks |
| Buffer overflow | Writing past allocated memory bounds, overwriting adjacent stack or heap pointers | Memory-safe languages, bounds checking, address space layout randomization (ASLR), and data execution prevention (DEP) or no-execute (NX) bits |
SORT IT OUT
The auditors read out four findings from older Meridian services. Which classic flaw is each one describing?
Show the answers
- Cross-site scripting. Untrusted data rendered in the browser without escaping is XSS. Encode output for its context and add a CSP.
- Cross-site request forgery. The browser attached the cookie on its own. Anti-CSRF tokens and SameSite cookies are the fix.
- Buffer overflow. Writing past allocated bounds overwrites adjacent pointers. Memory-safe languages and bounds checking prevent it.
- Injection. Untrusted input concatenated into a query is injection. Use a parameterized query.
CHECK YOUR JUDGMENT
After the review, you find the same pattern in Meridian's antenna-time scheduler. It checks that a tenant has minutes left, then deducts them in a separate step. Two requests sent 3 milliseconds apart both pass the check. Which change removes the root cause?
CASE FILE · CASE 11 · ENTRY 9 OF 9
Answer the closing question correctly to add this entry to your case file.
Check and debit, one step
The fuel check and debit now run as one locked transaction, the search query is parameterized, and the readiness review is clean: fix the cause, then prove it.