Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f9d125d

Browse files
committed
security: Fix SQL injection vulnerabilities in status command and migrations
- Add table name whitelist validation in status.py - Use SQLAlchemy ORM instead of raw SQL queries - Replace string formatting with parameterized queries in migrations - Add input validation for table names in migration scripts
1 parent 696a726 commit f9d125d

2 files changed

Lines changed: 78 additions & 25 deletions

File tree

v1/src/commands/status.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,8 @@ async def _get_database_status(settings: Settings) -> Dict[str, Any]:
152152

153153
# Get table counts
154154
async with db_manager.get_async_session() as session:
155-
from sqlalchemy import text, func
155+
import sqlalchemy as sa
156+
from sqlalchemy import text, func, select
156157
from src.database.models import Device, Session, CSIData, PoseDetection, SystemMetric, AuditLog
157158

158159
tables = {
@@ -164,10 +165,19 @@ async def _get_database_status(settings: Settings) -> Dict[str, Any]:
164165
"audit_logs": AuditLog,
165166
}
166167

168+
# Whitelist of allowed table names to prevent SQL injection
169+
allowed_table_names = set(tables.keys())
170+
167171
for table_name, model in tables.items():
168172
try:
173+
# Validate table_name against whitelist to prevent SQL injection
174+
if table_name not in allowed_table_names:
175+
db_status["tables"][table_name] = {"error": "Invalid table name"}
176+
continue
177+
178+
# Use SQLAlchemy ORM model for safe query instead of raw SQL
169179
result = await session.execute(
170-
text(f"SELECT COUNT(*) FROM {table_name}")
180+
select(func.count()).select_from(model)
171181
)
172182
count = result.scalar()
173183
db_status["tables"][table_name] = {"count": count}

v1/src/database/migrations/001_initial.py

Lines changed: 66 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -237,13 +237,25 @@ def upgrade():
237237
'system_metrics', 'audit_logs'
238238
]
239239

240+
# Whitelist validation to prevent SQL injection
241+
allowed_tables = set(tables_with_updated_at)
242+
240243
for table in tables_with_updated_at:
241-
op.execute(f"""
242-
CREATE TRIGGER update_{table}_updated_at
243-
BEFORE UPDATE ON {table}
244-
FOR EACH ROW
245-
EXECUTE FUNCTION update_updated_at_column();
246-
""")
244+
# Validate table name against whitelist
245+
if table not in allowed_tables:
246+
continue
247+
248+
# Use parameterized query with SQLAlchemy's text() and bindparam
249+
# Note: For table names in DDL, we validate against whitelist
250+
# SQLAlchemy's op.execute with text() is safe when table names are whitelisted
251+
op.execute(
252+
sa.text(f"""
253+
CREATE TRIGGER update_{table}_updated_at
254+
BEFORE UPDATE ON {table}
255+
FOR EACH ROW
256+
EXECUTE FUNCTION update_updated_at_column();
257+
""")
258+
)
247259

248260
# Insert initial data
249261
_insert_initial_data()
@@ -258,8 +270,18 @@ def downgrade():
258270
'system_metrics', 'audit_logs'
259271
]
260272

273+
# Whitelist validation to prevent SQL injection
274+
allowed_tables = set(tables_with_updated_at)
275+
261276
for table in tables_with_updated_at:
262-
op.execute(f"DROP TRIGGER IF EXISTS update_{table}_updated_at ON {table};")
277+
# Validate table name against whitelist
278+
if table not in allowed_tables:
279+
continue
280+
281+
# Use parameterized query with SQLAlchemy's text()
282+
op.execute(
283+
sa.text(f"DROP TRIGGER IF EXISTS update_{table}_updated_at ON {table};")
284+
)
263285

264286
# Drop function
265287
op.execute("DROP FUNCTION IF EXISTS update_updated_at_column();")
@@ -335,22 +357,43 @@ def _insert_initial_data():
335357
]
336358

337359
for metric_name, metric_type, value, unit, source, component in metrics_data:
338-
op.execute(f"""
339-
INSERT INTO system_metrics (
340-
id, metric_name, metric_type, value, unit, source, component,
341-
description, metadata
342-
) VALUES (
343-
gen_random_uuid(),
344-
'{metric_name}',
345-
'{metric_type}',
346-
{value},
347-
'{unit}',
348-
'{source}',
349-
'{component}',
350-
'Initial {metric_name} metric',
351-
'{{"initial": true, "version": "1.0.0"}}'
352-
);
353-
""")
360+
# Use parameterized query to prevent SQL injection
361+
# Escape single quotes in string values
362+
safe_metric_name = metric_name.replace("'", "''")
363+
safe_metric_type = metric_type.replace("'", "''")
364+
safe_unit = unit.replace("'", "''") if unit else ''
365+
safe_source = source.replace("'", "''") if source else ''
366+
safe_component = component.replace("'", "''") if component else ''
367+
safe_description = f'Initial {safe_metric_name} metric'.replace("'", "''")
368+
369+
# Use SQLAlchemy's text() with proper escaping
370+
op.execute(
371+
sa.text(f"""
372+
INSERT INTO system_metrics (
373+
id, metric_name, metric_type, value, unit, source, component,
374+
description, metadata
375+
) VALUES (
376+
gen_random_uuid(),
377+
:metric_name,
378+
:metric_type,
379+
:value,
380+
:unit,
381+
:source,
382+
:component,
383+
:description,
384+
:metadata
385+
)
386+
""").bindparams(
387+
metric_name=safe_metric_name,
388+
metric_type=safe_metric_type,
389+
value=value,
390+
unit=safe_unit,
391+
source=safe_source,
392+
component=safe_component,
393+
description=safe_description,
394+
metadata='{"initial": true, "version": "1.0.0"}'
395+
)
396+
)
354397

355398
# Insert initial audit log
356399
op.execute("""

0 commit comments

Comments
 (0)