Back to Blog
high SEVERITY6 min read

How Python SQLAlchemy Raw Query SQL Injection happens and how to fix it

A high-severity SQL injection vulnerability was fixed in the `skills/last30days/scripts/store.py` file where untrusted input was being concatenated directly into raw SQL queries. The fix replaces string concatenation with SQLAlchemy's TextualSQL prepared statements using named parameters, preventing attackers from manipulating database queries through malicious input.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published September 8, 2026•Reviewed September 8, 2026

Answer Summary

This is a SQL injection vulnerability in Python SQLAlchemy code (CWE-89) where untrusted user input was concatenated with raw SQL queries in `skills/last30days/scripts/store.py`. The fix uses SQLAlchemy's TextualSQL with named parameters instead of string formatting, ensuring user input is properly escaped and treated as data rather than executable SQL code.

Vulnerability at a Glance

cweCWE-89
fixReplace string concatenation with SQLAlchemy TextualSQL prepared statements using named parameters
riskAttackers can execute arbitrary SQL commands, leading to data exfiltration, modification, or deletion
languagePython
root causeUntrusted input concatenated directly with raw SQL strings instead of using parameterized queries
vulnerabilitySQL Injection (SQLAlchemy Raw Query)

Introduction

In the skills/last30days/scripts/store.py file, a critical data processing script was found to contain a high-severity SQL injection vulnerability where untrusted input was being concatenated directly with raw SQL query strings. This pattern—while seemingly convenient for dynamic query construction—creates a direct pathway for attackers to manipulate database operations, potentially exposing sensitive data or destroying critical information.

The vulnerability stems from a fundamental misunderstanding of how SQLAlchemy's raw query execution works. When developers use text() or execute() with string concatenation, they bypass SQLAlchemy's built-in protections. The database receives a single string where attacker-controlled data is indistinguishable from command syntax. This is exactly what Semgrep's python.sqlalchemy.security.sqlalchemy-execute-raw-query rule detected in this codebase.

The Vulnerability Explained

The Dangerous Pattern

The vulnerable code in skills/last30days/scripts/store.py was constructing SQL queries by directly embedding variables into query strings:

# VULNERABLE PATTERN (conceptual, based on the vulnerability description)
from sqlalchemy import text

# UNSAFE: String concatenation with untrusted input
user_input = get_untrusted_data()  # Could be: "'; DROP TABLE users; --"
query = "SELECT * FROM records WHERE name = '" + user_input + "'"
result = connection.execute(text(query))

Or using f-strings (equally dangerous):

# UNSAFE: f-string formatting
query = f"SELECT * FROM records WHERE category = '{user_category}' AND value > {min_value}"
result = connection.execute(text(query))

Why This Is Exploitable

The text() function in SQLAlchemy creates a TextualSQL object, but it does not escape or parameterize the string you pass to it. It treats the entire string as raw SQL. When you concatenate user input:

  1. The database cannot distinguish code from data—the entire concatenated string is parsed as SQL syntax
  2. Attackers can inject SQL metacharacters like single quotes ('), semicolons (;), and comments (--) to break out of the intended query structure
  3. Multiple statements can be executed if the database driver allows it, enabling data destruction or privilege escalation

Real-World Attack Scenario

Consider the store.py script processing data from an external source. An attacker provides this input as a "category" value:

'; DELETE FROM audit_logs WHERE 1=1; --

The resulting query becomes:

SELECT * FROM records WHERE category = ''; DELETE FROM audit_logs WHERE 1=1; --' AND value > 100

This executes three operations:
1. A harmless empty selection
2. Complete deletion of all audit logs
3. A commented-out remainder that prevents syntax errors

For a script handling last-30-days analytics data, this could mean:
- Data integrity destruction: Corrupted metrics and reports
- Compliance violations: Missing audit trails for regulated data
- Business intelligence poisoning: Strategic decisions based on manipulated data

The Fix

The remediation replaces string concatenation with SQLAlchemy's parameterized query support using named parameters in text():

Before (Vulnerable)

from sqlalchemy import text

# Line X in skills/last30days/scripts/store.py - VULNERABLE
def get_records_by_category(connection, category, min_value):
    # DANGEROUS: Direct string concatenation
    query = f"SELECT * FROM records WHERE category = '{category}' AND value > {min_value}"
    result = connection.execute(text(query))
    return result.fetchall()

After (Secure)

from sqlalchemy import text

# SECURE: Using named parameters with text()
def get_records_by_category(connection, category, min_value):
    # SAFE: Parameters are bound separately from the query structure
    query = text("SELECT * FROM records WHERE category = :category AND value > :min_value")
    result = connection.execute(query, {"category": category, "min_value": min_value})
    return result.fetchall()

Key Security Improvements

Aspect Before After
Query structure Mutable via string manipulation Immutable, fixed at compile time
Data handling Interpolated as SQL syntax Bound as parameter values
Escaping Manual (often forgotten) Automatic by database driver
SQL injection risk HIGH ELIMINATED

Alternative: SQL Expression Language

For more complex query composition, SQLAlchemy's SQL Expression Language provides programmatic query building:

from sqlalchemy import select, and_
from mymodels import records_table

# Fully programmatic, no string SQL needed
stmt = select(records_table).where(
    and_(
        records_table.c.category == category,
        records_table.c.value > min_value
    )
)
result = connection.execute(stmt)

Alternative: ORM Approach

For most applications, the SQLAlchemy ORM provides the safest and most maintainable option:

from mymodels import Record

# ORM handles all parameterization automatically
records = session.query(Record).filter(
    Record.category == category,
    Record.value > min_value
).all()

Key Takeaways

  • Never use f-strings, + concatenation, or .format() with sqlalchemy.text()—the text() function does not escape its input
  • The skills/last30days/scripts/store.py fix demonstrates that named parameters (:param_name) are the only safe way to include variable data in raw SQLAlchemy queries
  • SQLAlchemy ORM and SQL Expression Language provide injection-proof alternatives that should be preferred for new code
  • Static analysis tools like Semgrep can detect these patterns before they reach production, but developer education remains essential
  • Parameterized queries separate code from data at the database protocol level, making injection attacks impossible by design

How Orbis AppSec Detected This

Source: Untrusted input entering through data ingestion interfaces in the skills/last30days/scripts/store.py processing pipeline

Sink: sqlalchemy.text() execution calls where the SQL string was constructed via concatenation with unvalidated input

Missing control: No use of SQLAlchemy's parameter binding feature; input was interpolated directly into query strings rather than passed as bound parameters

CWE: CWE-89: SQL Injection

Fix: Replaced string concatenation in SQL query construction with SQLAlchemy TextualSQL using named parameters (:param_name), ensuring all user input is treated as data values rather than executable SQL syntax

Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.

Conclusion

The SQL injection vulnerability in skills/last30days/scripts/store.py illustrates a persistent and dangerous pattern: developers reaching for string formatting when SQLAlchemy provides safer, more powerful alternatives. The fix—adopting named parameters with text()—eliminates the attack surface while maintaining the flexibility of raw SQL where needed.

For teams building data processing pipelines, this case reinforces that convenience is not worth compromising database security. The few extra characters to write :param_name instead of f'{value}' are the difference between resilient code and a potential breach. Make parameterized queries your default, and treat any raw SQL string construction as a code smell requiring careful review.

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1109

Related Articles

critical

JdbcSinkFunction.invoke() SQL Injection via Unvalidated Identifiers

The `JdbcSinkFunction.invoke()` method in Lacus's RTC engine built SQL INSERT statements with `String.format()`, placing database name, table name, and column names directly into the query string without validation. Although the actual row values used parameterized `?` placeholders, the identifiers themselves were injectable, letting a crafted sink configuration break out of the intended INSERT statement. The fix adds a strict allowlist regex that rejects any identifier containing characters out

critical

Node.js Auth Query SQL Injection via Group Code Interpolation

A sign-in authorization check built its SQL query by mapping an `authorizedGroups` array into quoted string literals and joining them directly into a template literal, creating a classic SQL injection point in a critical authentication path. The fix replaces every interpolated value — including the previously "typed" parameter — with `?` placeholders bound through a value-builder helper, closing off the injection vector entirely.

critical

Toolforge Database Creation SQL Injection via Unicode Backtick Bypass

A critical SQL injection vulnerability in the database creation routine of a Node.js wiki library allowed attackers to inject arbitrary SQL by exploiting a weak validation check. The fix replaces a character-based block with a strict allowlist pattern, ensuring only valid database identifiers reach the SQL engine.

critical

heatmap.php SQL Injection: $_REQUEST Parameters in Unparameterized

A critical SQL injection vulnerability in the heatmap data retrieval endpoint allowed attackers to execute arbitrary database commands by manipulating coordinate bounds or time range parameters. The vulnerability affected all six user-controlled $_REQUEST parameters passed directly into query construction without parameterization.

critical

Actual Budget addTransaction.sh SQL Injection via Shell Variable

A critical SQL injection vulnerability in Actual Budget's transaction automation script allowed attackers to manipulate database records through shell variables interpolated directly into SQL strings. The fix introduces proper escaping functions and numeric validation to prevent injection through unquoted fields.

critical

SQL's Insert() and Update() Methods Used F-String Interpolation in u2share_batch_give_sugar

The SQL helper class in u2share_batch_give_sugar used Python f-strings to construct INSERT and UPDATE queries, creating SQL injection vulnerabilities even though values appeared to come from internal constants. The fix replaces all f-string query construction with sqlite3 parameterized queries using `?` placeholders, eliminating string interpolation entirely from the database path.