← Back to Blog

The Limits of SAST: When Secure Code Still Builds an Insecure System

appsecsaststatic-analysissecure-design

SAST is exceptionally good at finding security problems that are expressed in code. That is also its fundamental limitation.

Some of the most important application vulnerabilities aren't caused by a dangerous function, an unsafe API, or a tainted data flow. They exist because a security control was never implemented in the first place.

SAST can tell you that code does something dangerous. It is much harder for SAST to tell you that code failed to do something security-critical.

The Problem of the Missing Control

Consider a file-upload endpoint. A developer writes something perfectly reasonable:

Python
@app.post("/profile-picture")
def upload(file):
    path = save(file)
    return {"url": path}

No SQL injection. No hardcoded password. No command injection. The SAST scan is clean.

But the actual security requirements were:

⚠ Blind Spot

The code isn't bad code. The application is missing security decisions. That's a fundamentally different problem for SAST.

IDOR: A Perfect Example

SAST correctly determines the query below is safe from SQL injection. But in a multi-tenant application, the real vulnerability is what's absent.

SQL
SELECT *
FROM invoices
WHERE id = :invoice_id;
-- Parameterized. No injection.
-- SAST: clean.

◆ Key Insight

The question isn't “Is this query dangerous?” — it's “Is this user allowed to see this particular row?” That's a contextual question, not a syntactic one.

Authentication ≠ Authorization

The endpoint checks whether the user is logged in. Good. But for a POST /api/users/change-email — should it also require:

Rate Limiting

Python
@app.post("/password-reset")
def reset(request):
    ...

The implementation is fine. But what happens when someone sends:

10 requests
100 requests
10,000 requests
1,000,000 requests

⚠ Blind Spot

The vulnerability is not inside the function. It's the absence of an appropriate abuse-control mechanism — and “appropriate” depends entirely on context. There is no static-analysis rule that says “this endpoint should permit exactly 17 requests per minute.”

Business Logic

The client sends a price. The code accepts it. Everything is syntactically valid.

JSON
{
  "product_id": 123,
  "quantity": 1,
  "price": 10
}

Race Conditions

Sequentially, this looks perfectly fine:

Python
balance = get_balance(account)

if balance >= amount:
    update_balance(account, balance - amount)
    transfer(amount)

But security doesn't happen sequentially. Step through the concurrent path:

Race Condition — step through

1 / 5
›Balance: ₹10,000

⚠ Blind Spot

The vulnerability emerges from the interaction between executions, not from the code itself. This is why dynamic testing, architecture review, and threat modeling remain essential even with extensive SAST coverage.

Configuration Is Another Boundary

Some of the most important security controls aren't in source code at all — they're in infrastructure: S3 bucket permissions, IAM policies, security groups, Kubernetes RBAC, TLS config, CSP headers, HSTS.

Internet
   ↓
Public S3 bucket
   ↓
Sensitive customer data

✓ Takeaway

You can have a perfect SAST score while accidentally shipping this. SAST isn't failing — it was never designed to answer infrastructure questions.

The Human Problem With SAST

I've seen security engineers with limited development experience move into SAST roles and treat scanner output as a vulnerability report rather than as an input into security analysis. That creates two opposite failure modes:

Over-reporting

A finding gets reported without checking if the input is attacker-controlled, if the path is reachable, or if another control already exists. Technically matches a rule. Not actually exploitable.

Under-reporting

"SAST didn't flag it, therefore it's safe." Particularly dangerous for missing controls — the scanner may not complain about unrestricted file upload because it's not a single dangerous API call.

◆ Key Insight

The skill that matters: looking at code and asking “What security control should exist here that doesn't?” — a fundamentally different skill from knowing how to operate a SAST product.

The LLM Helps — But Doesn't Solve It

LLM-assisted SAST is substantially more interesting than rule-based scanners because it can reason across controllers, models, middleware, routes, and infra definitions simultaneously. Given:

Python
save_uploaded_file(file)

An LLM might ask: where is the file-type validation? Size restriction? Malware scan? Where is it stored? Can another user retrieve it?

✓ Takeaway

LLMs reduce the gap. They don't eliminate it.

The Real Boundary: Security Properties vs. Code Patterns

Security is ultimately about properties the system must maintain. Click each one to see why SAST struggles with it:

So What Should We Do With SAST?

The answer isn't to abandon it. A mature approach uses all four lenses together:

SAST

"What potentially dangerous patterns exist in this code?"

DAST

"What can I actually make the running application do?"

Threat Modeling

"What could go wrong given the architecture and attacker?"

Security Review

"What controls should exist, and are they actually implemented?"

✓ Takeaway

And experienced application-security engineers connect all four.

The Uncomfortable Conclusion

A clean SAST report means something much narrower than “this application is secure.” It means:

The application didn't violate the security rules that the SAST engine was capable of recognizing from the information it had.

That's useful. It's just not the same thing.

The most interesting vulnerabilities are often not “the developer wrote dangerous code.” They're “the developer never implemented the security property the system required.”

No scanner can reliably discover every requirement that nobody encoded.

◆ Key Insight

SAST is a security signal, not a security verdict. The goal isn't to find more findings — it's to determine whether the application actually maintains the security properties it is supposed to maintain. That's where static analysis ends, and actual application security begins.