The Limits of SAST: When Secure Code Still Builds an Insecure System
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:
@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
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.
SELECT *
FROM invoices
WHERE id = :invoice_id;
-- Parameterized. No injection.
-- SAST: clean.◆ Key Insight
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
@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
Business Logic
The client sends a price. The code accepts it. Everything is syntactically valid.
{
"product_id": 123,
"quantity": 1,
"price": 10
}Race Conditions
Sequentially, this looks perfectly fine:
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⚠ Blind Spot
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
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 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:
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
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
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