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
Published September 8, 2026Reviewed 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()

Prevention & Best Practices

1. Never Concatenate Into SQL Strings

Establish this as a hard rule in your team:

# ❌ NEVER DO THIS
text(f"SELECT * FROM t WHERE x = '{user_input}'")
text("SELECT * FROM t WHERE x = '" + user_input + "'")
text("SELECT * FROM t WHERE x = '%s'" % user_input)

# ✅ ALWAYS DO THIS
text("SELECT * FROM t WHERE x = :param").bindparams(param=user_input)

2. Use Type-Safe Query Builders

Prefer the SQL Expression Language or ORM over raw SQL when possible. These APIs make SQL injection structurally impossible:

# SQL Expression Language - no string SQL to inject into
from sqlalchemy import select, table, column
t = table('records', column('id'), column('name'))
stmt = select(t).where(t.c.name == user_input)  # Safe by construction

3. Static Analysis in CI/CD

Add Semgrep to your pipeline to catch these patterns automatically:

# .github/workflows/security.yml
- name: Run Semgrep
  uses: returntocorp/semgrep-action@v1
  with:
    config: >-
      p/python
      p/owasp-top-ten

The specific rule that found this vulnerability: python.sqlalchemy.security.sqlalchemy-execute-raw-query

4. Defense in Depth: Least Privilege

Even with parameterized queries, database connections should use principle of least privilege:

# Use read-only users for analytics scripts
# Separate credentials for write operations

5. Security Standards Alignment

Standard Reference
OWASP Top 10 2021 A03:2021 – Injection
CWE CWE-89: SQL Injection
OWASP Cheat Sheet SQL Injection Prevention Cheat Sheet

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.

References

Frequently Asked Questions

What is SQLAlchemy raw query SQL injection?

It's when untrusted data is concatenated into SQLAlchemy's `text()` or `execute()` calls, allowing attackers to inject malicious SQL commands that the database executes.

How do you prevent SQL injection in Python SQLAlchemy?

Use SQLAlchemy's TextualSQL with named parameters (`:param_name`), the SQL Expression Language, or the ORM. Never use f-strings, `+` concatenation, or `.format()` with untrusted input in SQL queries.

What CWE is SQLAlchemy raw query SQL injection?

CWE-89: SQL Injection

Is input sanitization enough to prevent SQL injection in SQLAlchemy?

No. Manual sanitization is error-prone and insufficient. Always use parameterized queries/prepared statements where the database engine handles escaping.

Can static analysis detect SQLAlchemy raw query SQL injection?

Yes. Tools like Semgrep, Bandit, and CodeQL can detect patterns like `text(f"...")` or string concatenation into SQLAlchemy query methods.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1109

Related Articles

critical

How SQL injection happens in Python DuckDB view creation and how to fix it

A critical SQL injection flaw in `python/src/idx/api.py:265` built five DuckDB `CREATE VIEW` statements with Python f-strings, interpolating a filesystem path directly into SQL text. The fix replaces the interpolated path with a bound parameter (`read_parquet(?)`) and moves the view names into a hardcoded, non-interpolated statement map — eliminating any path where filenames or directory values can alter SQL structure.

critical

How SQL Injection happens in PHP bulk email systems and how to fix it

A critical SQL injection vulnerability in `admin/utilities/bulkEmailSystem.php` allowed attackers to inject arbitrary SQL through unvalidated database names passed from user input. The fix implements strict input validation using regex pattern matching to ensure only safe database identifiers are processed, preventing exploitation of the bulk email functionality.

high

How SQL Injection Happens in Shell Scripts and How to Fix It

A critical SQL injection vulnerability in the `check_kuota.sh` script allowed attackers to execute arbitrary SQL commands by controlling the USERNAME parameter. The fix implements strict input validation using regex pattern matching to ensure only legitimate username characters are accepted, eliminating the injection vector entirely.

critical

How SQL Injection happens in JavaScript template literals and how to fix it

A critical SQL injection vulnerability in `index.js` allowed attackers to execute arbitrary database commands by manipulating block IDs passed through the UI. The fix implements strict input validation using a regex whitelist before any SQL construction, eliminating the injection vector while preserving functionality.

high

How SQL Injection via Template Literals happens in TypeScript and how to fix it

A high-severity SQL injection vulnerability was discovered in the admin panel's database tools where schema names were directly interpolated into SQL queries using JavaScript template literals. The fix replaced unsafe string concatenation with a proper `quoteSchemaLiteral()` function to sanitize inputs before query construction, eliminating the injection vector in two critical database inspection functions.

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.