@@ -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