Scope: Data-layer security guidance for Python applications using SQLAlchemy (ORM & Core). Covers parameterized query construction,
text()bindings, tenant query scoping, bulk update mass assignment, session lifecycle, and transaction isolation.
SQLAlchemy ORM query methods (filter(), filter_by()) parameterize queries automatically. However, using raw SQL clauses with text() requires explicit parameter binding.
# VULNERABLE: Direct f-string interpolation into raw SQL
from sqlalchemy import text
def find_user_by_email(session, email: str):
query = text(f"SELECT * FROM users WHERE email = '{email}'") # ❌ SQL Injection
return session.execute(query).fetchall()# SAFE: Named parameter binding
from sqlalchemy import text
def find_user_by_email(session, email: str):
query = text("SELECT * FROM users WHERE email = :email") # ✅ Parameterized
return session.execute(query, {"email": email}).fetchall()Never concatenate wildcards directly into the SQL string:
# ❌ UNSAFE: query = text(f"SELECT * FROM docs WHERE title LIKE '%{user_term}%'")
# ✅ SAFE: Bind wildcard parameter in the dictionary mapping
def search_documents(session, user_term: str):
query = text("SELECT * FROM docs WHERE title LIKE :pattern")
return session.execute(query, {"pattern": f"%{user_term}%"}).fetchall()An ORM query is not secure if it omits tenant or user boundaries.
# VULNERABLE: Any caller can access any order by ID
def get_order(session, order_id: int):
return session.query(Order).filter(Order.id == order_id).first()# SAFE: Scope query strictly to the current tenant / user
def get_order(session, order_id: int, current_user_id: int):
return session.query(Order).filter(
Order.id == order_id,
Order.user_id == current_user_id
).first()Avoid passing raw dictionary payloads into .update().
# VULNERABLE: Accepts arbitrary dict fields directly into update
def update_profile(session, user_id: int, client_data: dict):
session.query(User).filter(User.id == user_id).update(client_data)# SAFE: Explicit column whitelist
ALLOWED_FIELDS = {'bio', 'display_name', 'phone_number'}
def update_profile(session, user_id: int, client_data: dict):
sanitized_updates = {k: v for k, v in client_data.items() if k in ALLOWED_FIELDS}
session.query(User).filter(User.id == user_id).update(sanitized_updates)- Always manage session lifecycles using context managers (
with Session() as session:or FastAPI dependencies) to ensure sessions and connections are closed. - Ensure failed operations roll back cleanly to avoid leaving database connections in aborted transaction states.
- All
text()constructs use:parambindings rather than f-strings or.format(). - LIKE searches bind wildcards in the parameter values rather than concatenating into SQL.
- Multi-tenant queries include tenant/user ID filter conditions.
- Bulk
.update()queries validate and whitelist editable fields. - Sessions are scoped to request lifecycles and close cleanly upon completion.
- Connection pool sizes and timeouts are configured to prevent connection pool exhaustion.