Why SAST Misses Business Logic Bugs

Why SAST Misses Business Logic Bugs

Static analysis is good at finding bugs that have a shape. Feed it your source code and it will spot a SQL string built from user input or a password committed straight into a file. But the moment you ask about sast business logic coverage, the story changes, because the worst logic bugs have no bad shape to match. The code is clean, it does exactly what it says, and it is still wrong.

What SAST is actually good at

SAST (static application security testing) reads your code without running it and traces how data moves. It follows a value from where it enters, like a request parameter, to where it gets used, like a database call. When tainted input reaches a dangerous function with no cleaning in between, it flags the path.

Here is the kind of code it catches every time:

name = request.args.get("name")
query = "SELECT * FROM users WHERE name = '" + name + "'"
db.execute(query)

The user controls name, it lands in a raw SQL string, and the scanner follows that line from source to sink. The same works for command injection, unsafe deserialization, weak crypto calls, and hardcoded secrets. All of these share one trait: there is a recognizable pattern in the text. A concatenated query looks dangerous. A private key in a string literal looks dangerous. SAST is a pattern engine, and these are patterns, so it does well.

Why sast business logic coverage falls apart

Business logic bugs do not have a pattern. The code is syntactically clean and does what the developer wrote. The problem is not a dangerous line that is present. The problem is a safe line that is missing, and absence of code has no shape for a matcher to find.

Walk through four examples. Each one passes every static check and is still a real hole.

An IDOR with a perfectly safe query

Consider this handler in an invented invoicing app called Acme Billing:

@app.get("/api/invoices/<int:invoice_id>")
def get_invoice(invoice_id):
    row = db.execute(
        "SELECT * FROM invoices WHERE id = ?", (invoice_id,)
    ).fetchone()
    return jsonify(row)

SAST loves this code. The query is parameterized, so there is no injection. The input is an integer, so the type check passes. Nothing here matches a bad pattern. But a user who owns invoice 41 can request GET /api/invoices/42 and read someone else’s invoice, because the handler never checks who owns the row. This is an insecure direct object reference, a form of business logic vulnerability. The missing line is an ownership check, and a missing line has no signature.

A negative quantity that breaks an invariant

A checkout endpoint accepts a quantity and multiplies it by a price:

total = item.price * request.json["quantity"]
account.balance += total

Every type check passes. The quantity is an integer, the price is a number, the math is valid. Send {"quantity": -3} and the total goes negative, so the buyer gets credited instead of charged. The rule the app assumed, that quantity is always positive, is nowhere in the code. SAST cannot flag a broken invariant it was never told about.

A refund flow you can replay

A refund endpoint marks an order refunded and pays the customer. It reads the order, issues the payment, then writes the status. If two requests arrive at once, or the same request is sent twice, both can pass the status read before either writes it, and the customer gets paid twice. The code is clean. There is no injection, no bad function call. The flaw is a missing lock and a missing idempotency key, and again the bug is what is not there.

A workflow step you can skip

A signup has three steps: verify email, accept terms, then activate. The activate route trusts that the first two ran. Nothing stops a caller from posting straight to POST /activate and skipping ahead. The state machine lives in the developer’s head, not in a shape the scanner can read.

The root reason SAST cannot see these

Put the four together and the pattern is that there is no pattern. SAST knows how to spot dangerous shapes. It does not know what your app is supposed to do. It has no idea that invoice 42 belongs to a different user, that quantity must be positive, that a refund should happen once, or that activation comes last. Those are rules about intent, and intent is not written in the syntax.

A static scanner can prove your query is safe. It cannot prove your app enforced the rule you never wrote down.

This is why the fair contrast is not people versus tools. It is pattern matching versus understanding. Signature based static analysis asks one question: does this code contain a known bad shape? A logic bug answers no, honestly, and slips through. To be clear, this is not a reason to drop SAST. It catches real injection and secret bugs early and cheaply on every commit, and that is worth keeping. It just has a ceiling, and business logic sits above it.

What closes the gap

The fix is not a longer list of bad patterns. You cannot write a signature for a check that should exist but does not. What closes the gap is modeling what the app is meant to do and then testing whether its assumptions hold.

  • Learn the intended rules. Ownership, allowed value ranges, once only actions, required order of steps. These are the invariants the code quietly trusts.
  • Form ideas about where they break. If invoice reads are keyed by id, ask whether id is checked against the caller. If a refund writes state after paying, ask what happens on a replay.
  • Test the assumption, not a payload. Send GET /api/invoices/42 as the wrong user and see if a real invoice comes back. Replay the refund and see if the balance moves twice.

That is reasoning about the app, not scanning it for shapes. If you want to go deeper on where pattern tools stop and this kind of work begins, read our writing in scanners vs research, and the tool comparison in SAST vs DAST vs IAST.

UnboundCompute is built for exactly this gap. It learns how an app is meant to work, forms ideas about where that logic breaks, and tests those assumptions instead of matching a fixed payload list. You can read how we think about it on our about page.

Frequently asked questions

Why does SAST miss business logic bugs?

SAST is a pattern engine. It finds code that has a dangerous shape, like a SQL string built from user input or a secret in a literal. Business logic bugs have no bad shape. The code is clean and does what it says, and the flaw is a missing check the scanner was never told to expect. Absence of code has no pattern to match.

What kinds of bugs does SAST find well?

SAST is strong on bugs with a recognizable pattern in the source: SQL and command injection where tainted input reaches a sink, unsafe deserialization, weak crypto calls, and hardcoded secrets. These all share a visible signature in the text of the program, so a static matcher can trace and flag them early on every commit.

Can SAST catch an IDOR or broken access control?

Usually no. Consider GET /api/invoices/42 handled by a parameterized query with an integer id. There is no injection and the type check passes, so SAST sees clean code. The bug is the missing ownership check, and a missing line has no signature for a static scanner to find.

What actually finds business logic bugs?

Understanding, not pattern matching. You model what the app is meant to do, its ownership rules, value ranges, once only actions, and required step order, then test whether those assumptions hold. That means sending a request as the wrong user or replaying a refund and checking the result, rather than matching a fixed payload list.


Put an autonomous researcher on your own systems

UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.

Free and open source: the security-agent-skills library packages 33 tool-agnostic security-testing skills for AI coding agents, encoding the testing method behind attacks like the one in this post. Read how it works or get it on GitHub.