@@ -10,13 +10,14 @@ import (
1010
1111// DBExecStep executes parameterized SQL INSERT/UPDATE/DELETE against a named database service.
1212type DBExecStep struct {
13- name string
14- database string
15- query string
16- params []string
17- ignoreError bool
18- app modular.Application
19- tmpl * TemplateEngine
13+ name string
14+ database string
15+ query string
16+ params []string
17+ ignoreError bool
18+ allowDynamicSQL bool
19+ app modular.Application
20+ tmpl * TemplateEngine
2021}
2122
2223// NewDBExecStepFactory returns a StepFactory that creates DBExecStep instances.
@@ -32,8 +33,10 @@ func NewDBExecStepFactory() StepFactory {
3233 return nil , fmt .Errorf ("db_exec step %q: 'query' is required" , name )
3334 }
3435
35- // Safety: reject template expressions in SQL to prevent injection
36- if strings .Contains (query , "{{" ) {
36+ // Safety: reject template expressions in SQL to prevent injection,
37+ // unless allow_dynamic_sql is explicitly enabled.
38+ allowDynamicSQL , _ := config ["allow_dynamic_sql" ].(bool )
39+ if ! allowDynamicSQL && strings .Contains (query , "{{" ) {
3740 return nil , fmt .Errorf ("db_exec step %q: query must not contain template expressions (use params instead)" , name )
3841 }
3942
@@ -51,20 +54,33 @@ func NewDBExecStepFactory() StepFactory {
5154 ignoreError , _ := config ["ignore_error" ].(bool )
5255
5356 return & DBExecStep {
54- name : name ,
55- database : database ,
56- query : query ,
57- params : params ,
58- ignoreError : ignoreError ,
59- app : app ,
60- tmpl : NewTemplateEngine (),
57+ name : name ,
58+ database : database ,
59+ query : query ,
60+ params : params ,
61+ ignoreError : ignoreError ,
62+ allowDynamicSQL : allowDynamicSQL ,
63+ app : app ,
64+ tmpl : NewTemplateEngine (),
6165 }, nil
6266 }
6367}
6468
6569func (s * DBExecStep ) Name () string { return s .name }
6670
6771func (s * DBExecStep ) Execute (_ context.Context , pc * PipelineContext ) (* StepResult , error ) {
72+ // Resolve template expressions in the query early (before any DB access) when
73+ // dynamic SQL is enabled. This validates resolved identifiers against an
74+ // allowlist before any database interaction.
75+ query := s .query
76+ if s .allowDynamicSQL {
77+ var err error
78+ query , err = resolveDynamicSQL (s .tmpl , query , pc )
79+ if err != nil {
80+ return nil , fmt .Errorf ("db_exec step %q: %w" , s .name , err )
81+ }
82+ }
83+
6884 if s .app == nil {
6985 return nil , fmt .Errorf ("db_exec step %q: no application context" , s .name )
7086 }
@@ -102,7 +118,7 @@ func (s *DBExecStep) Execute(_ context.Context, pc *PipelineContext) (*StepResul
102118
103119 // Normalize SQL placeholders: users write $1,$2,$3 (PostgreSQL style),
104120 // engine converts to ? for SQLite automatically.
105- query : = normalizePlaceholders (s . query , driver )
121+ query = normalizePlaceholders (query , driver )
106122
107123 // Execute statement
108124 result , err := db .Exec (query , resolvedParams ... )
0 commit comments