Lambda SQL Injection via SQLAlchemy execute()

CRITICAL

Lambda event data flows to SQLAlchemy session.execute() or connection.execute() without bound parameters, enabling SQL injection against any RDS backend.

Rule Information

Language
Python
Category
AWS Lambda
Author
Shivasurya
Shivasurya
Last Updated
2026-03-22
Tags
pythonawslambdasql-injectionsqlalchemyrdstaint-analysisinter-proceduralCWE-89OWASP-A03
CWE References

Interactive Playground

Experiment with the vulnerable code and security rule below. Edit the code to see how the rule detects different vulnerability patterns.

pathfinder scan --ruleset python/PYTHON-LAMBDA-SEC-014 --project .
1
2
3
4
5
6
7
rule.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

About This Rule

Understanding the vulnerability and how it is detected

This rule detects SQL injection vulnerabilities in AWS Lambda functions where untrusted event data flows into SQLAlchemy session.execute() or connection.execute() calls without bound parameters, enabling SQL injection against RDS MySQL, PostgreSQL, SQL Server, or any SQLAlchemy-supported backend.

SQLAlchemy is widely used in Lambda functions for its ORM capabilities and support for multiple database backends. When used correctly with the ORM or with text() and bound parameters, SQLAlchemy prevents injection. However, two unsafe patterns are common in Lambda handlers: passing a raw f-string SQL query to text() without bound parameters, and passing event data directly to execute() as an unparameterized string.

Lambda functions receive event data from API Gateway, SQS, SNS, S3, and other triggers (event.get("body"), event.get("queryStringParameters"), event["Records"]). This data is attacker-controllable. When it is embedded in the SQL string argument to text() or execute() via f-strings or concatenation, the injection bypasses all SQLAlchemy protections since SQLAlchemy only escapes values that are passed through its bound parameter mechanism (text() with :parameter syntax and bindparams()).

Security Implications

Potential attack scenarios if this vulnerability is exploited

1

False Safety from SQLAlchemy's Reputation

Developers often assume that using SQLAlchemy automatically prevents SQL injection. This is true for ORM operations (query(), filter(), Session.add()) but not for raw SQL via execute(). When event data is embedded in a string passed to text() or execute() directly, SQLAlchemy provides no injection protection. The false sense of security from using SQLAlchemy can cause security reviews to overlook these vulnerable call sites.

2

Multi-Database Backend Exposure

SQLAlchemy supports MySQL, PostgreSQL, SQL Server, Oracle, and SQLite. A Lambda function using SQLAlchemy with any of these backends is equally vulnerable to injection through unparameterized execute() calls. The attacker's injection payload adapts to the specific SQL dialect, enabling database-specific attack techniques on each backend.

3

Full Database Access via Session Context

SQLAlchemy sessions are often configured with broad database permissions for ORM convenience. An injected SQL string through session.execute() executes with all permissions of the session's database user, which may include read/write access to all tables in the schema.

4

ORM Bypass via Raw execute()

The presence of SQLAlchemy ORM models and safe ORM queries in the same Lambda function does not protect against injection in raw execute() calls. Both safe and unsafe call sites can coexist in the same Lambda handler, and only the unsafe raw execute() calls with event data need to be fixed.

How to Fix

Recommended remediation steps

  • 1Always use SQLAlchemy's text() with :parameter_name syntax and pass values as a dictionary to execute(), never embed event data in the SQL string.
  • 2Prefer SQLAlchemy ORM methods (session.query(), session.get(), filter()) over raw SQL to benefit from automatic parameterization.
  • 3Validate and type-convert event fields before use (e.g., int() for numeric IDs) as a defense-in-depth measure.
  • 4Grant the Lambda's database user the minimum necessary privileges; avoid broad permissions even when using SQLAlchemy ORM.
  • 5Initialize the SQLAlchemy engine at module level for connection reuse across warm Lambda invocations.

Detection Scope

How Code Pathfinder analyzes your code for this vulnerability

This rule performs inter-procedural taint analysis with global scope. Sources are Lambda event dictionary access calls: calls("event.get"), calls("event.__getitem__"), including event.get("body"), event.get("queryStringParameters"), event.get("pathParameters"), and event["Records"]. Sinks are calls("*.execute") matching SQLAlchemy session or connection objects with the tainted SQL string tracked via .tracks(0). The rule flags cases where event data reaches the SQL string argument without being passed through the bound parameters dictionary. Sanitizers include explicit int() or float() type conversion. The analysis follows taint through f-strings, concatenation, variable assignments, and module boundaries.

Compliance & Standards

Industry frameworks and regulations that require detection of this vulnerability

CWE Top 25
CWE-89 ranked #3 in 2023 Most Dangerous Software Weaknesses
OWASP Top 10
A03:2021 - Injection
PCI DSS v4.0
Requirement 6.2.4 - protect against injection attacks
NIST SP 800-53
SI-10: Information Input Validation
AWS Security Best Practices
Validate all inputs; apply least-privilege database permissions

References

External resources and documentation

Similar Rules

Explore related security rules for Python

Frequently Asked Questions

Common questions about Lambda SQL Injection via SQLAlchemy execute()

Yes for ORM operations: session.query(User).filter(User.id == user_id), session.get(), and relationship-based queries all use parameterization internally. No for raw SQL: session.execute("SELECT * FROM users WHERE id = " + user_id) bypasses all ORM protections. The rule specifically targets raw SQL execute() calls with event data embedded in the string argument, which are the dangerous pattern even when ORM is used elsewhere in the same Lambda function.
Use named parameters with colon syntax in the SQL string and pass values as a dictionary: session.execute(text("SELECT * FROM t WHERE id = :id"), {"id": user_id}). SQLAlchemy's bindparams() method on the text() object is an alternative: text("SELECT * FROM t WHERE id = :id").bindparams(id=user_id). Both correctly use parameterization. Never use Python f-strings or % formatting to embed values in the string passed to text().
Create the engine at module level so it is reused across warm invocations. Use pool_pre_ping=True to test connections before use. For high-concurrency Lambda functions, set pool_size and max_overflow conservatively or use NullPool to create new connections per invocation. AWS RDS Proxy is the recommended solution for connection pooling at Lambda scale, as it handles connection reuse outside the Lambda execution environment.
Yes. SQLAlchemy's Core expression language (select(), where(), and_(), or_()) builds parameterized queries programmatically using column objects rather than string concatenation. Dynamic filters built with and_() and column objects are safe. The injection risk is specifically with raw SQL strings (text() with f-strings, or execute() with concatenated strings). Use Core expressions for dynamic query construction instead.
The injection risk applies equally to all backends. The fix is the same for each engine: use text() with :param syntax and pass values as dictionaries. If different Lambda functions connect to different backends, each one must be audited independently. Consider using a shared database utilities module that enforces parameterization for all execute() calls across all backends.

New feature

Get these findings posted directly on your GitHub pull requests

The Lambda SQL Injection via SQLAlchemy execute() rule runs in CI and posts inline review comments on the exact lines — no dashboard, no SARIF viewer.

See how it works