Skip to content

Commit 242c07b

Browse files
authored
fix(clickhouse): validate identifiers to prevent SQL injection (#693)
* fix(clickhouse): validate identifiers to prevent SQL injection * chore: address review feedback * fix(clickhouse): enforce non-empty table name * test: remove invalid empty identifier test case
1 parent 88971e0 commit 242c07b

2 files changed

Lines changed: 53 additions & 0 deletions

File tree

tools/clickhouse.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,18 @@ const (
3131
ClickHouseFormatTable = 1
3232
)
3333

34+
var clickHouseIdentifierRe = regexp.MustCompile(`^[a-zA-Z0-9_]+$`)
35+
36+
func validateClickHouseIdentifier(name, field string) error {
37+
if name == "" {
38+
return nil
39+
}
40+
if !clickHouseIdentifierRe.MatchString(name) {
41+
return fmt.Errorf("invalid %s: must contain only letters, numbers, and underscores", field)
42+
}
43+
return nil
44+
}
45+
3446
// ClickHouseQueryParams defines the parameters for querying ClickHouse
3547
type ClickHouseQueryParams struct {
3648
DatasourceUID string `json:"datasourceUid" jsonschema:"required,description=The UID of the ClickHouse datasource to query. Use list_datasources to find available UIDs."`
@@ -398,6 +410,10 @@ type ClickHouseTableInfo struct {
398410

399411
// listClickHouseTables lists tables from a ClickHouse datasource
400412
func listClickHouseTables(ctx context.Context, args ListClickHouseTablesParams) ([]ClickHouseTableInfo, error) {
413+
if err := validateClickHouseIdentifier(args.Database, "database"); err != nil {
414+
return nil, err
415+
}
416+
401417
// Build the query to list tables
402418
query := `SELECT database, name, engine, total_rows, total_bytes
403419
FROM system.tables
@@ -475,6 +491,18 @@ func describeClickHouseTable(ctx context.Context, args DescribeClickHouseTablePa
475491
database = "default"
476492
}
477493

494+
if err := validateClickHouseIdentifier(database, "database"); err != nil {
495+
return nil, err
496+
}
497+
498+
if args.Table == "" {
499+
return nil, fmt.Errorf("table is required")
500+
}
501+
502+
if err := validateClickHouseIdentifier(args.Table, "table"); err != nil {
503+
return nil, err
504+
}
505+
478506
// Query system.columns instead of using DESCRIBE TABLE
479507
// This avoids the LIMIT clause issue with DESCRIBE statements
480508
query := fmt.Sprintf(`SELECT name, type, default_kind as default_type, default_expression, comment

tools/clickhouse_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,3 +333,28 @@ func TestGenerateClickHouseEmptyResultHints(t *testing.T) {
333333
}
334334
assert.True(t, found, "Hints should suggest using list_clickhouse_tables")
335335
}
336+
337+
func TestValidateClickHouseIdentifier(t *testing.T) {
338+
tests := []struct {
339+
name string
340+
field string
341+
wantErr bool
342+
}{
343+
{name: "default", field: "database", wantErr: false},
344+
{name: "table_1", field: "table", wantErr: false},
345+
346+
//attack payloads (must fail)
347+
{name: "default' OR 1=1 --", field: "database", wantErr: true},
348+
{name: "default' UNION SELECT name FROM system.databases --", field: "database", wantErr: true},
349+
{name: "table-name", field: "table", wantErr: true},
350+
{name: "table name", field: "table", wantErr: true},
351+
{name: "system.tables", field: "table", wantErr: true},
352+
}
353+
354+
for _, tt := range tests {
355+
err := validateClickHouseIdentifier(tt.name, tt.field)
356+
if (err != nil) != tt.wantErr {
357+
t.Errorf("validateClickHouseIdentifier(%q) error = %v, wantErr %v", tt.name, err, tt.wantErr)
358+
}
359+
}
360+
}

0 commit comments

Comments
 (0)