criticalUser input reaching your database or shell
SQL Built With f-strings, format() or Concatenation
SQL statements are built by putting values straight into the query text.
Why it matters
The database cannot tell where your SQL ends and the caller's value begins. A quote character in the input closes your string and the rest is executed as SQL. Placeholders send the value separately, so it is always treated as data. Every Python database driver supports them.
What it looks like
This is the shape of code that triggers the rule. AI tools produce it because it works, and nothing tells them it is unsafe.
f-string inside execute
query = request.args.get('search')
cursor = conn.execute(f"SELECT * FROM users WHERE name = '{query}'")The smallest fix
minimal patch
# Before
cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")
# After: the driver substitutes the value safely
cursor.execute("SELECT * FROM users WHERE name = %s", (name,)) # psycopg2 / MySQL
cursor.execute("SELECT * FROM users WHERE name = ?", (name,)) # sqlite3The better fix
If you have a few more minutes, this is the approach that holds up as the app grows.
safe alternative
# Use the ORM or SQLAlchemy Core so you never write the string yourself:
user = db.session.execute(select(User).where(User.name == name)).scalar_one_or_none()
# Or bound parameters with text():
db.session.execute(text("SELECT * FROM users WHERE name = :name"), {"name": name})Let your AI tool fix it
When the scanner finds this in your project, it fills in the file and line for you. This is the prompt it gives you to paste into Claude Code, Cursor or whatever you use.
In [the file] at line [the line number] a SQL statement is built with string formatting: [the flagged code]. Rewrite it with driver placeholders (%s for psycopg2 and MySQL, ? for sqlite3, :name for SQLAlchemy text()) and pass the values as a separate tuple or dict. Do the same for every execute() call in this file. If the table or column name itself must vary, check it against a fixed allow-list instead of formatting it in.
How to check the fix worked
1. Call the endpoint with a value containing a single quote and `OR 1=1` and confirm it returns no extra rows and no 500. 2. Confirm the normal value still returns the expected row.