Test an authorized web application for business-logic flaws and race conditions — the classes scanners cannot find. Model invariants (conservation of value, uniqueness, monotonicity, exclusivity), build an Actor x Action x Resource matrix, abuse state machines (skip/reorder/replay steps, limit-slicing), find numeric bugs (float-vs-decimal rounding, cross-currency refund arbitrage, negative amounts), stale feature gates, and saga gaps; prove concurrency bugs (double-spend, lost update, idempotency-key reuse) with controlled parallel requests. Use when the user mentions 'business logic testing,' 'race condition,' 'double spend,' 'TOCTOU,' 'concurrency bug,' 'idempotency,' 'coupon reuse,' 'workflow bypass,' 'limit bypass,' 'negative amount,' 'refund arbitrage,' 'parallel request attack,' or 'logic flaw.'
96%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Passed
No findings from the security scan
The vulnerability classes scanners cannot find. A scanner matches syntax; a logic flaw is a mismatch between what the app is supposed to guarantee and what its code actually enforces under adversarial or concurrent use. This skill is the dynamic counterpart to owasp-audit's source-side race patterns (TOCTOU, atomic queue claim, reserve-then-act) and the depth behind web-pentest Phase 8.
Work invariant-first: name the guarantee, then try to break it. A finding is not "I sent a weird request" — it is "I violated a stated invariant and proved durable state changed."
Concurrency and logic testing mutates real state. Before sending anything:
If any of these is missing, stop and ask. See Boundaries below.
Sending live requests is deliberately outside this skill's pre-approved tools (Read, Grep, Glob only) — the human runs the parallel requests, or approves each tool invocation, so a throughput test never fires unattended.
Before touching a request, write down what the system promises. Four families cover most money and workflow bugs:
For each invariant, note where it is enforced (a DB unique constraint? an application if? only the UI?) and what proves a violation (a ledger that no longer sums to zero, two rows where one should exist). An invariant enforced only in the UI or only in a single handler is the lead.
Enumerate the grid and look for the cells the app forgot:
Fill each cell with the expected outcome (allow / deny), then test the deny cells and the state-sensitive allow cells. The interesting failures: a non-owner reaching an owner action (overlaps IDOR/BOLA — see web-pentest Phase 4), an actor performing an action valid only in a different state, or the same actor performing a once-only action twice.
Draw the intended state graph (cart -> checkout -> paid -> shipped; draft -> submitted -> approved). Then attack the edges:
paid without a charge, or approved without a reviewer?approver_id / reviewer_id so the requester and approver are the same principal.Each of these is a candidate for the concurrency amplifier in Step 6 — a window that is hard to hit sequentially is trivial to hit with parallel requests.
0.1 + 0.2, long-fraction quantities, and values near rounding boundaries; watch for a cent appearing or vanishing.-1, transfer a negative sum (crediting yourself), or apply a discount larger than the subtotal to drive the total below zero?1e3), extra precision, leading zeros, or integer overflow at the boundary of the money type.Most logic invariants are enforced with a check-then-act sequence that is safe sequentially and unsafe in parallel. The goal is to land N requests inside the same check-then-act window so the check passes N times before any act commits.
Patterns to hunt (source-side equivalents live in owasp-audit A06/A02/A08):
(endpoint, key) rather than (principal, endpoint, key), another user's key — or a replay of your own — reuses a completed result. Submit two different operations under one key; submit one key across two accounts.balance = read(); write(balance - amount) with no atomic decrement or version check — concurrent writers each start from the same read and the last write wins, losing the others' effect.NX/EX or fencing tokens. A lock set without atomic acquire-and-expire, or without a fencing token, lets two holders believe they own it (one acquires, the other sets over it; or a GC-paused holder acts after its lease expired).SELECT ... then INSERT (instead of a DB UNIQUE constraint) breaks here.INSERT ... ON CONFLICT) double-inserts under concurrency.Landing the requests on the same window. The web-request equivalent of a starter pistol:
Tooling, a worked Turbo Intruder config, and a minimal httpx/curl --parallel runner are in references/concurrency.md — all scoped to authorized-test use and the agreed request cap.
Hold every candidate to the evidence bar before it becomes a finding. Use the verdict vocabulary: a candidate is confirmed, needs_validation, or rejected (see references/verification.md). Severity attaches only to confirmed findings; calibrate with references/severity-calibration.md.
General validation standard (all classes): 1 request is correctly denied, then N concurrent requests succeed where only 1 should — and the durable state change is proven (ledger no longer sums correctly, two rows where uniqueness required one, a counter past its monotone bound, a balance that moved further than any single authorized action allows). A changed response code alone is not proof; read the persisted state back.
| Class | Confirmed when | Likely false positive |
|---|---|---|
| Double-spend / coupon reuse | Coupon with N uses redeemed > N times; refunded value exceeds charged; inventory oversold — proven by reading the balance/count back | The op is genuinely idempotent — enforced ETag/If-Match, or a DB UNIQUE/ON CONFLICT collapses the duplicates so only one commits |
| Lost update | Sum of concurrent deltas does not equal the net change in the durable balance | An atomic decrement or a version/WHERE balance >= amount guard rejected all but one |
| Idempotency-key reuse | A second distinct operation (or another principal) reused a key and got the first result, or a replay double-applied | Keys correctly scoped to principal + request body hash; replays return the cached result without re-applying the side effect |
| State-machine skip/reorder/replay | A terminal/forbidden state reached without its precondition, persisted | The transition was rejected server-side; only the UI advanced |
| Limit-slicing | Aggregate exceeds the limit across sliced requests, durably | A running server-side total caught the aggregate |
| Numeric (negative/rounding/arbitrage) | Final persisted balance reflects created or destroyed value | Input validation rejected it; the "discrepancy" was display formatting only |
| Stale gate / saga gap | A revoked capability still performed a durable action, or services left inconsistent after an interrupted/duplicated flow | A short, documented cache TTL that closes on its own with no durable effect |
The two false-positive traps to always rule out:
If-Match, a DB UNIQUE constraint, or INSERT ... ON CONFLICT DO NOTHING means the duplicates collapse to one commit — the parallel requests look accepted (all 200) but only one took effect. Always read the durable state, never trust the status code.A candidate you could not land under the agreed request cap is needs_validation with a validation_plan (the exact burst to run in staging), not a confirmed finding.
Report with the shared findings vocabulary; see references/findings-format.md for the findings.json record and how to validate/export it.
# Business Logic & Race Condition Test Report
## Target / environment: [app + staging vs prod, test-mode confirmation]
## Authorization: [scope reference, request-count cap]
### Invariant register
| Invariant | Family | Enforced where | Violation proof |
### Findings
#### [verdict] [Title]
**Invariant violated:** [conservation / uniqueness / monotonicity / exclusivity]
**Endpoint(s):** `METHOD /path`
**Preconditions:** [accounts, state, concurrency needed]
**Reproduction:** [the controlled burst — count, sync method; or the step sequence]
**Durable-state proof:** [the balance/row/counter read back that shows the violation]
**Verdict:** confirmed | needs_validation (+ blockers, validation_plan) | rejected (+ ruled_out_by {control, location})
**Severity:** [only if confirmed — see severity-calibration.md]
### Hardening notes (not findings)
[Visual-only glitches, defense-in-depth gaps with no traced durable impact]web-pentest Boundaries).owasp-audit A06/A02/A08 — source-side race patterns: TOCTOU, atomic queue claim (UPDATE ... WHERE status='pending' RETURNING), reserve-then-act, external-resource-create TOCTOU. Read the code to locate the check-then-act window this skill proves dynamically.web-pentest Phase 8 (business logic) and its "Combined white-box + black-box mode" — when source and a running instance are both available, code findings point to the exact window to race.api-audit — idempotency, BOLA, and API-level object/function authorization.finding-triage — close each candidate to a disposition with contextual severity.vulnerabilities/race_conditions.md and vulnerabilities/business_logic.md (https://github.com/usestrix/strix). Techniques paraphrased, not copied. This skill deliberately does not adopt Strix's never-refuse / never-question-authorization posture: authorization, staging preference, and request caps are enforced here.briiirussell/cybersecurity-skills
c79bb8b
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.