Skip to content

Commit 4951585

Browse files
author
Faycal Bououza
committed
Security: prevent execution of write SQL statements
1 parent d1c1f5f commit 4951585

File tree

3 files changed

+83
-0
lines changed

3 files changed

+83
-0
lines changed

main.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,10 @@ func runExport(cmd *cobra.Command, args []string) error {
157157
logger.Debug("Using inline SQL query (%d characters)", len(query))
158158
}
159159

160+
if err := validateQuery(query); err != nil {
161+
return err
162+
}
163+
160164
format = strings.ToLower(strings.TrimSpace(format))
161165

162166
var delimRune rune = ','

store.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"net/url"
7+
"strings"
78
"time"
89

910
"github.com/fbz-tec/pgexport/logger"
@@ -119,3 +120,38 @@ func sanitizeURL(dbUrl string) string {
119120

120121
return fmt.Sprintf("%s://%s%s%s", u.Scheme, userInfo, u.Host, path)
121122
}
123+
124+
// ValidateQuery checks if the query is safe for export (read-only)
125+
func validateQuery(query string) error {
126+
// Normalize query to uppercase for checking
127+
normalized := strings.ToUpper(strings.TrimSpace(query))
128+
129+
// List of forbidden SQL commands
130+
forbiddenCommands := []string{
131+
"DELETE",
132+
"DROP",
133+
"TRUNCATE",
134+
"INSERT",
135+
"UPDATE",
136+
"ALTER",
137+
"CREATE",
138+
"GRANT",
139+
"REVOKE",
140+
}
141+
142+
// Check if query starts with forbidden command
143+
for _, cmd := range forbiddenCommands {
144+
if strings.HasPrefix(normalized, cmd) {
145+
return fmt.Errorf("forbidden SQL command detected: %s (read-only mode)", cmd)
146+
}
147+
}
148+
149+
// Additional check: detect forbidden keywords anywhere in query
150+
for _, cmd := range forbiddenCommands {
151+
if strings.Contains(normalized, cmd+" ") || strings.Contains(normalized, cmd+";") {
152+
return fmt.Errorf("forbidden SQL command detected in query: %s", cmd)
153+
}
154+
}
155+
156+
return nil
157+
}

store_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,49 @@ func TestConnectionReuse(t *testing.T) {
367367
}
368368
}
369369

370+
func TestValidateQuery(t *testing.T) {
371+
tests := []struct {
372+
name string
373+
query string
374+
wantErr bool
375+
}{
376+
{
377+
name: "valid SELECT",
378+
query: "SELECT * FROM users",
379+
wantErr: false,
380+
},
381+
{
382+
name: "forbidden DELETE",
383+
query: "DELETE FROM users",
384+
wantErr: true,
385+
},
386+
{
387+
name: "forbidden DROP",
388+
query: "DROP TABLE users",
389+
wantErr: true,
390+
},
391+
{
392+
name: "chained DELETE",
393+
query: "SELECT 1; DELETE FROM users",
394+
wantErr: true,
395+
},
396+
{
397+
name: "lowercase delete",
398+
query: "delete from users",
399+
wantErr: true,
400+
},
401+
}
402+
403+
for _, tt := range tests {
404+
t.Run(tt.name, func(t *testing.T) {
405+
err := validateQuery(tt.query)
406+
if (err != nil) != tt.wantErr {
407+
t.Errorf("validateQuery() error = %v, wantErr %v", err, tt.wantErr)
408+
}
409+
})
410+
}
411+
}
412+
370413
// Helper function to get test database URL from environment
371414
// Set DB_TEST_URL environment variable to run integration tests
372415
// Example: export DB_TEST_URL="postgres://user:pass@localhost:5432/testdb"

0 commit comments

Comments
 (0)