SQL injection static analysis
Why regex-based SAST misses multi-hop SQL injection
SQL injection rarely announces itself on one line. In real code, user input can move through handlers, helpers, query builders, and only later reach the database.
The bug does not fit on one line
Here is a small Flask example:
from flask import request
def normalize_email(value):
return value.strip().lower()
def user_lookup_clause(email):
return f"email = '{email}'"
@app.route("/users")
def find_user():
email = normalize_email(request.args["email"])
where = user_lookup_clause(email)
sql = "SELECT id, email FROM users WHERE " + where
return db.execute(sql)
The dangerous operation is at the bottom:
db.execute(sql)
But the reason it is dangerous starts several lines earlier:
request.args["email"]
The vulnerability is the path between those two points.
Why pattern matching struggles
A pattern rule can search for string concatenation near a SQL call. That catches some bugs. It can also search for request parameters near a query. That catches some bugs too.
But this example splits the problem across functions:
- The request value enters in
find_user. normalize_emailchanges the string but does not make it safe for SQL.user_lookup_clausebuilds part of the SQL statement.db.executeruns the final query.
No single line tells the whole story.
What taint tracking does differently
Taint tracking follows data from a source, such as request input, through assignments, helper calls, string construction, and return values. Then it checks whether that value reaches a sensitive sink, such as SQL execution.
If user-controlled input reaches a SQL execution call without parameterization, the scanner has a real reason to flag the code. The important difference is not that SiteShadow sees a suspicious function name. It is that SiteShadow follows the flow.
The fix is parameterization
The safer version keeps the SQL structure and the user value separate:
from flask import request
@app.route("/users")
def find_user():
email = request.args["email"].strip().lower()
sql = "SELECT id, email FROM users WHERE email = %s"
return db.execute(sql, (email,))
The database driver treats the email as data, not executable SQL. That is the kind of fix developers should see immediately: what is wrong, why it matters, and what safe code looks like.
Where SiteShadow fits
SiteShadow is built for developers who want security feedback inside their workflow. It combines taint tracking, security rules, heuristic checks, and AI/LLM rule families to help find real risks while code is still fresh in the editor.
Those numbers are not the story. The story is simpler: if user input becomes a query, command, file path, template, redirect, or model instruction, SiteShadow should help you see the risk before review.