Skip to content

security(gateway): prevent SQL injection in schema introspection query builders - #1625

Open
matheusfrancisco wants to merge 1 commit into
mainfrom
Fix-Injection
Open

security(gateway): prevent SQL injection in schema introspection query builders#1625
matheusfrancisco wants to merge 1 commit into
mainfrom
Fix-Injection

Conversation

@matheusfrancisco

Copy link
Copy Markdown
Contributor
  • The DynamoDB columns path interpolated tableName into a shell command (aws dynamodb describe-table --table-name %s) [0/6603]
    unvalidated.

Since these scripts execute against the target database via clientexec with the connection's credentials, a crafted identifier
allowed predicate manipulation or statement injection.

Validation now lives inside the query builders themselves (validateSchemaIdentifier: alphanumeric, _, -, ., 1–128 chars —
rejects backticks, quotes, whitespace, and separators), so every call path is covered regardless of caller behavior.
TablesQueryFor/ColumnsQueryFor return (string, error); callers surface the error as HTTP 422 or an MCP tool error.

📣 User-facing impact

None — security hardening of internal schema introspection; no behavior change for valid database, table, or schema names.

🔗 Related Issue

Fixes #

🚀 Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)

📋 Changes Made

  • gateway/api/connections/queries_schema.go: added validateSchemaIdentifier; getTablesQuery/getColumnsQuery (and exported
    TablesQueryFor/ColumnsQueryFor) now validate every interpolated identifier and return (string, error)
  • gateway/api/connections/helpers.go: validateDatabaseName delegates to validateSchemaIdentifier (same regex, same error message)
    so the two cannot drift
  • gateway/api/connections/database_explorer.go: ListTables/GetTableColumns handle the builder error and return HTTP 422;
    table/schema are now validated on the columns endpoint
  • gateway/api/mcpserver/tools_schema.go: connectionTablesHandler/connectionColumnsHandler handle the builder error and return an
    MCP tool error result
  • gateway/api/connections/queries_schema_test.go: added injection-rejection tests (backtick/quote breakouts per connection type) and
    a valid-identifier acceptance test

🧪 Testing

Ran go test ./gateway/api/connections/ ./gateway/api/mcpserver/ -count=1 — all pass, including pre-existing
TestValidateDatabaseName and MySQL quoting tests. Full go build ./gateway/... ./common/... clean; repo-wide grep confirms no other
callers of the changed signatures.

Test Configuration:

  • Browser(s): N/A (backend only)
  • OS: macOS (darwin/arm64)

Tests performed:

  • Unit tests pass
  • Integration tests pass
  • Manual testing completed

📸 Screenshots (if applicable)

N/A

✅ Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings
  • New and existing unit tests pass locally with my changes
  • I have checked my code and corrected any misspellings

📄 Additional Notes

  • Reviewers: focus on getColumnsQuery — validation is per connection type, matching exactly the identifiers each builder interpolates
    (e.g. MongoDB ignores schema, OracleDB ignores database, DynamoDB uses only table).
  • The identifier charset is identical to the old validateDatabaseName regex, so no previously accepted name is rejected.
  • Label suggestion: patch (security fix, no API surface change).

@matheusfrancisco matheusfrancisco added the patch Bumps the patch version on release (bug fixes) label Jul 22, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Migration Safety Analysis

No database migrations were changed in this PR. Safe to deploy to sandbox.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Harden schema introspection builders against identifier injection

🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Validate database/schema/table identifiers before building introspection scripts.
• Propagate validation failures to HTTP (422) and MCP tool errors.
• Add unit tests to reject common breakout/injection payloads across connection types.
Diagram

graph TD
  A["HTTP schema endpoints"] --> B["Schema query builders"] --> C["Generated script"] --> D["clientexec"] --> E[("Target DB / AWS CLI")]
  F["MCP schema tools"] --> B
  B --> G["Identifier validator"]
  subgraph Legend
    direction LR
    _svc["Handler/Service"] ~~~ _mod["Module"] ~~~ _db[("External system")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Validate only at request boundary (handlers)
  • ➕ Simple call sites; avoids changing query builder signatures
  • ➖ Easy to miss non-HTTP call paths (e.g., MCP); validation can drift across endpoints
2. Use parameterized queries / driver metadata APIs instead of script interpolation
  • ➕ Eliminates most injection classes by avoiding string interpolation
  • ➕ Potentially more portable if using standard driver capabilities
  • ➖ Bigger refactor; requires per-database driver support and connection plumbing changes
  • ➖ Not always applicable for CLI-based flows like DynamoDB
3. Per-connection-type identifier allowlists (stricter than shared regex)
  • ➕ Can match each backend’s exact identifier rules (case, length, allowed chars)
  • ➖ More maintenance burden; increased risk of inconsistent behavior across backends

Recommendation: Keep the PR’s approach: validating inside the query builders ensures every execution path (HTTP + MCP + future callers) is covered, and returning (string, error) makes failures explicit. Given the current architecture relies on generated scripts (including AWS CLI), this is the highest ROI hardening without a broad redesign.

Files changed (5) +186 / -54

Bug fix (3) +106 / -43
database_explorer.goSurface schema-script validation failures as HTTP 422 +9/-7

Surface schema-script validation failures as HTTP 422

• Updates ListTables and GetTableColumns to handle (script, error) from the query builders. Removes DynamoDB columns CLI string formatting from the handler path so table-name validation is enforced centrally.

gateway/api/connections/database_explorer.go

queries_schema.goAdd identifier allowlist and return errors from schema query builders +89/-34

Add identifier allowlist and return errors from schema query builders

• Introduces validateSchemaIdentifier (allowlist regex) and applies it to every interpolated database/schema/table name across supported connection types. Changes TablesQueryFor/ColumnsQueryFor and internal builders to return (string, error) and updates MySQL builder comments to reflect the new validation flow.

gateway/api/connections/queries_schema.go

tools_schema.goPropagate schema-script validation failures as MCP tool errors +8/-2

Propagate schema-script validation failures as MCP tool errors

• Updates MCP schema tool handlers to consume (script, error) from apiconnections and return tool error results when validation fails. Preserves existing unsupported-connection-type behavior when scripts are empty.

gateway/api/mcpserver/tools_schema.go

Refactor (1) +3 / -11
helpers.goDelegate database name validation to shared schema identifier validator +3/-11

Delegate database name validation to shared schema identifier validator

• Replaces the local database-name regex check with a call to validateSchemaIdentifier to prevent rule drift. Keeps the existing user-facing error message consistent.

gateway/api/connections/helpers.go

Tests (1) +77 / -0
queries_schema_test.goAdd tests rejecting injection payloads and accepting safe identifiers +77/-0

Add tests rejecting injection payloads and accepting safe identifiers

• Adds coverage to ensure getTablesQuery/getColumnsQuery reject quotes/backticks/whitespace and other unsafe inputs across multiple connection types. Includes a positive test confirming valid identifiers produce non-empty scripts.

gateway/api/connections/queries_schema_test.go

@github-actions

Copy link
Copy Markdown
Contributor

📋 API Changelog

API Changelog unknown vs. unknown

No changes detected

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 26 rules

Grey Divider


Remediation recommended

1. Oracle missing params misreported 🐞 Bug ≡ Correctness
Description
With the new OracleDB schema-name validation, GetTableColumns requests that omit both schema and
database now fail as HTTP 422 "invalid schema name" because the handler defaults schemaName to
dbName (which can be empty for Oracle). This is a confusing client-facing error path for what is
effectively a missing required parameter for Oracle column introspection.
Code

gateway/api/connections/queries_schema.go[R99-105]

+		if err := validateSchemaIdentifier("table name", tableName); err != nil {
+			return "", err
+		}
+		if err := validateSchemaIdentifier("schema name", schemaName); err != nil {
+			return "", err
+		}
+		return getOracleDBColumnsQuery(tableName, schemaName), nil
Relevance

⭐⭐⭐ High

Repo often accepts improving client-facing API errors/status handling (avoid confusing 422/404
paths); likely accept missing-param clarification.

PR-#1107
PR-#1020

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
database_explorer.go shows OracleDB is excluded from the needsDbName check and that an omitted
schema is defaulted from dbName; queries_schema.go shows OracleDB now rejects empty
schemaName, causing a 422 validation error for requests with both params omitted.

gateway/api/connections/database_explorer.go[352-392]
gateway/api/connections/queries_schema.go[98-105]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`getColumnsQuery` now validates `schemaName` for OracleDB and rejects empty values. In `GetTableColumns`, OracleDB does not require `database`, and `schemaName` defaults to `dbName` when omitted—so requests with neither `schema` nor `database` will now return a generic 422 "invalid schema name".

## Issue Context
This behavior is introduced by the new validation; prior behavior would execute a query with an empty owner filter (likely returning no results). Now it fails early but with a misleading message compared to an explicit “missing schema/database” response.

## Fix Focus Areas
- gateway/api/connections/database_explorer.go[352-392]
- gateway/api/connections/queries_schema.go[98-105]

## Suggested fix
- In `GetTableColumns`, add an OracleDB-specific guard before calling `getColumnsQuery`, e.g.:
 - If `currentConnectionType == pb.ConnectionTypeOracleDB` and `schema` is empty, require one explicit source of schema (either `schema` query param, or decide to treat `database` as schema and require it to be non-empty).
 - Return a clear client error message (and consistent status) like `"schema parameter is required for OracleDB"` instead of surfacing the generic validator error.
- Keep the validator in `getColumnsQuery` (do not relax it), so injection protection remains intact.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. Validator error can drift 🐞 Bug ⚙ Maintainability
Description
validateDatabaseName delegates acceptance to validateSchemaIdentifier but discards the returned
error and emits its own hardcoded message, so future changes to the shared validator’s
constraints/message can diverge silently. This undermines the stated goal of having a single
canonical validator for schema identifiers.
Code

gateway/api/connections/helpers.go[R365-369]

+	// Allows only letters, numbers, underscores, hyphens, and dots with
+	// length between 1 and 128 characters (see validateSchemaIdentifier).
+	if err := validateSchemaIdentifier("database name", dbName); err != nil {
		return fmt.Errorf("invalid database name. Only alphanumeric characters, underscore, hyphen and dot are allowed with length between 1 and 128 characters")
	}
-
Relevance

⭐⭐⭐ High

Team previously accepted fixing validation/message mismatches (helpers.go) to keep errors
consistent; likely accept avoiding drift.

PR-#1240
PR-#1110

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
helpers.go shows the delegated error being discarded and replaced with a separate message, while
queries_schema.go defines the canonical error format used by the new query builder validations.

gateway/api/connections/helpers.go[363-371]
gateway/api/connections/queries_schema.go[17-24]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`validateDatabaseName` calls `validateSchemaIdentifier("database name", dbName)` but ignores its returned error and returns a separate hardcoded error string. This reintroduces duplication and can drift if `validateSchemaIdentifier` changes.

## Issue Context
`validateSchemaIdentifier` is intended to be the canonical schema identifier validator used by the schema query builders.

## Fix Focus Areas
- gateway/api/connections/helpers.go[363-371]
- gateway/api/connections/queries_schema.go[17-24]

## Suggested fix
- Replace the `if err := validateSchemaIdentifier(...); err != nil { return fmt.Errorf(...) }` block with `return validateSchemaIdentifier("database name", dbName)` (or wrap it, if you need to preserve the exact historical string), ensuring there is a single source of truth for both validation and messaging.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +365 to 369
// Allows only letters, numbers, underscores, hyphens, and dots with
// length between 1 and 128 characters (see validateSchemaIdentifier).
if err := validateSchemaIdentifier("database name", dbName); err != nil {
return fmt.Errorf("invalid database name. Only alphanumeric characters, underscore, hyphen and dot are allowed with length between 1 and 128 characters")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Informational

1. Validator error can drift 🐞 Bug ⚙ Maintainability

validateDatabaseName delegates acceptance to validateSchemaIdentifier but discards the returned
error and emits its own hardcoded message, so future changes to the shared validator’s
constraints/message can diverge silently. This undermines the stated goal of having a single
canonical validator for schema identifiers.
Agent Prompt
## Issue description
`validateDatabaseName` calls `validateSchemaIdentifier("database name", dbName)` but ignores its returned error and returns a separate hardcoded error string. This reintroduces duplication and can drift if `validateSchemaIdentifier` changes.

## Issue Context
`validateSchemaIdentifier` is intended to be the canonical schema identifier validator used by the schema query builders.

## Fix Focus Areas
- gateway/api/connections/helpers.go[363-371]
- gateway/api/connections/queries_schema.go[17-24]

## Suggested fix
- Replace the `if err := validateSchemaIdentifier(...); err != nil { return fmt.Errorf(...) }` block with `return validateSchemaIdentifier("database name", dbName)` (or wrap it, if you need to preserve the exact historical string), ensuring there is a single source of truth for both validation and messaging.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +99 to +105
if err := validateSchemaIdentifier("table name", tableName); err != nil {
return "", err
}
if err := validateSchemaIdentifier("schema name", schemaName); err != nil {
return "", err
}
return getOracleDBColumnsQuery(tableName, schemaName), nil

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Oracle missing params misreported 🐞 Bug ≡ Correctness

With the new OracleDB schema-name validation, GetTableColumns requests that omit both schema and
database now fail as HTTP 422 "invalid schema name" because the handler defaults schemaName to
dbName (which can be empty for Oracle). This is a confusing client-facing error path for what is
effectively a missing required parameter for Oracle column introspection.
Agent Prompt
## Issue description
`getColumnsQuery` now validates `schemaName` for OracleDB and rejects empty values. In `GetTableColumns`, OracleDB does not require `database`, and `schemaName` defaults to `dbName` when omitted—so requests with neither `schema` nor `database` will now return a generic 422 "invalid schema name".

## Issue Context
This behavior is introduced by the new validation; prior behavior would execute a query with an empty owner filter (likely returning no results). Now it fails early but with a misleading message compared to an explicit “missing schema/database” response.

## Fix Focus Areas
- gateway/api/connections/database_explorer.go[352-392]
- gateway/api/connections/queries_schema.go[98-105]

## Suggested fix
- In `GetTableColumns`, add an OracleDB-specific guard before calling `getColumnsQuery`, e.g.:
  - If `currentConnectionType == pb.ConnectionTypeOracleDB` and `schema` is empty, require one explicit source of schema (either `schema` query param, or decide to treat `database` as schema and require it to be non-empty).
  - Return a clear client error message (and consistent status) like `"schema parameter is required for OracleDB"` instead of surfacing the generic validator error.
- Keep the validator in `getColumnsQuery` (do not relax it), so injection protection remains intact.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@sandromello

Copy link
Copy Markdown
Contributor

✅ Build Completed with Success, Version=1625.0.0-g17b44ba

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

patch Bumps the patch version on release (bug fixes)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants