# ARGUS-A01: Unsafe SQL Concatenation
> **Rule Code:** `ARGUS-A01`
> **Identifier:** `UNSAFE_SQL_CONCATENATION`
> **Severity:** `CRITICAL` (Security Blocker)
> **Category:** `Security & Data Integrity`
> **Target Standards:** CWE-89 (SQL Injection), OWASP ASVS v4.0.3/v5.0 §V5.3.1, OWASP Top 10 A03:2021 (Injection)
---
## 1. Overview & Core Invariant
Every SQL query string executed through database drivers (such as `pgx` or `database/sql`) **must** be a compile-time constant string literal or assembled strictly with **parameterized placeholders (`$1, $2, ...`)**.
Dynamic string concatenation using the `+` operator, `fmt.Sprintf`, `fmt.Sprint`, `strings.Join`, `strings.Builder`, or custom string formatters on runtime arguments is strictly prohibited. This invariant guarantees absolute, physical isolation between the **SQL AST grammar structure** and **runtime data values**.
---
## 2. Technical Grounding & PostgreSQL Engine Realities
In PostgreSQL architectures, query execution through the **Extended Query Protocol v3.0** operates across distinct phases:
1. **Parse:** The server parses the SQL statement containing `$n` parameter symbols into a prepared statement and stores it in the query plan cache.
2. **Bind:** Concrete parameter values (in binary or text format) are bound directly to parameter slots without re-parsing the SQL AST grammar.
3. **Describe & Execute:** The query executor runs the pre-compiled execution plan with negligible latency.
```mermaid
flowchart TD
subgraph SAFE ["Extended Query Protocol v3.0 (SAFE)"]
direction TB
AppSafe["Client Application"] -->|"1. Parse: 'SELECT ... WHERE id = $1'"| ParserSafe["PostgreSQL C Parser"]
ParserSafe -->|"Cached Execution Plan"| Cache["Generic Plan Cache"]
AppSafe -->|"2. Bind: Param[1] = '12345'"| ExecSafe["Engine Executor"]
Cache -.->|"Reuse Plan"| ExecSafe
ExecSafe -->|"Safe & Fast Execution"| DB1[("Database")]
end
subgraph DANGER ["Dynamic String Concatenation (CATASTROPHIC RISK)"]
direction TB
AppDanger["Client Application"] -->|"Concat: 'SELECT ... WHERE id = ' + input"| ParserDanger["PostgreSQL C Parser"]
ParserDanger -->|"Forces Re-parsing on Every Call"| Thrash["Plan Cache Thrashing & CPU Spikes"]
ParserDanger -->|"Grammar Boundaries Manipulated"| Vuln["SQL Injection Vulnerability (CWE-89)"]
end
```
When queries are concatenated dynamically, every invocation generates a distinct SQL string. This triggers **Plan Cache Thrashing** inside `src/backend/tcop/postgres.c`, starving database CPU and opening critical injection vectors.
### 2.1. The Multi-Statement Protection Boundary
Under the Extended Query Protocol v3.0, PostgreSQL's `Parse` message strictly enforces a single prepared statement per command. If an attacker injects a command delimiter (`;`) intending to execute stacked queries (e.g. `'; DROP TABLE users; --`), the PostgreSQL engine immediately rejects the command with:
```
ERROR: cannot insert multiple commands into a prepared statement
```
Dynamic string concatenation undermines this fundamental database engine invariant by conflating AST code structure with untrusted runtime values.
### 2.2. Generic Plan vs. Custom Plan Optimization
When using parameterized queries (`$1, $2, ...`), PostgreSQL evaluates whether to switch to a re-usable **Generic Plan** after 5 executions. When queries are dynamically concatenated with runtime literals:
- PostgreSQL is forced to generate a brand new **Custom Plan** on every single request.
- The shared plan cache (`pg_stat_statements`) suffers catastrophic bloat and eviction churn.
- Database CPU spikes dramatically under concurrent OLTP workloads.
---
## 3. How Argus Detects Violations (Taint Flow Architecture)
Argus utilizes an intra-procedural AST taint-tracking engine to trace untrusted input from source to execution sink:
```mermaid
flowchart LR
Source["Taint Sources
(Request DTOs, HTTP Params, Filter Structs)"] -->|"Variable Assignment & Strings.Builder"| Prop["Taint Propagation
(Binary '+', fmt.Sprintf, strings.Join)"]
Prop -->|"Unsanitized SQL String"| Sink["Database Sink
(pool.Query, QueryRow, Exec)"]
Prop -.->|"pgx.Identifier.Sanitize() or $%d placeholder"| Sanitizer["Sanitizer / Safe Builder"]
Sanitizer -.->|"Safe Query"| Sink
```
1. **Taint Sources:**
- Function parameters carrying request payloads (e.g., parameter names: `id`, `email`, `param`, `query`, `filter`, `input`, `body`, `req`, `request`).
- Types ending with `Request`, `DTO`, `Input`, `Params`, as well as `*http.Request`.
2. **Taint Propagators:**
- Variable assignments (`:=`, `=`) across multiple intermediate assignments.
- String concatenation operators (`+`).
- Dynamic buffer mutations (`strings.Builder.WriteString`, `strings.Builder.Write`).
- Unbounded string formatting (`fmt.Sprintf` with `%s` or `%v`, `strings.Join`).
3. **Legitimate Sanitizers:**
- Formal identifier sanitization: `pgx.Identifier{table}.Sanitize()` or `SanitizeIdentifier(name)`.
- Parameter counter formatting: `fmt.Sprintf(" AND col = $%d", idx)` where only numeric parameter positions are formatted into the query text.
4. **Execution Sinks:**
- Method calls matching `Query`, `QueryRow`, or `Exec` on database executor interfaces or `pgxpool.Pool`.
---
## 4. Vulnerability & Risk Taxonomy
| Attack Vector / Failure Mode | Technical Impact | Risk Level |
| :--------------------------- | :----------------------------------------------------------------------------------- | :----------- |
| **SQL Injection (CWE-89)** | Arbitrary SQL command execution, data breach, authentication bypass, data tampering. | **CRITICAL** |
| **Plan Cache Thrashing** | Continuous CPU load and memory fragmentation due to endless uncacheable query plans. | **HIGH** |
| **Lexer Token Manipulation** | Unescaped quotes (`'`) or operators break query semantic boundaries unexpectedly. | **HIGH** |
---
## 5. Non-Compliant Code Patterns (Bad Examples)
### Example 1: Direct Binary String Concatenation
```go
// VIOLATION: Concatenating runtime input directly with operator '+'
func GetUserByID(ctx context.Context, pool *pgxpool.Pool, id string) (*User, error) {
query := "SELECT id, username, email FROM users WHERE id = '" + id + "'"
row := pool.QueryRow(ctx, query)
// ...
}
```
### Example 2: String Formatting via `fmt.Sprintf`
```go
// VIOLATION: Using %s format specifier to interpolate string arguments
func DeleteUser(ctx context.Context, pool *pgxpool.Pool, r *http.Request) error {
userID := r.URL.Query().Get("id")
query := fmt.Sprintf("DELETE FROM users WHERE id = '%s'", userID)
_, err := pool.Exec(ctx, query)
return err
}
```
### Example 3: Unsafe Dynamic `strings.Builder`
```go
// VIOLATION: Appending raw input into a strings.Builder query buffer
var sb strings.Builder
sb.WriteString("SELECT id, name FROM users WHERE status = '")
sb.WriteString(req.Status) // Tainted variable
sb.WriteString("'")
rows, err := pool.Query(ctx, sb.String())
```
---
## 6. Compliant Implementation Patterns (Good Examples)
### Solution 1: Static Parameterized Query (Standard)
```go
// COMPLIANT: Query constant with parameterized placeholder $1
func GetUserByID(ctx context.Context, pool *pgxpool.Pool, id string) (*User, error) {
const query = "SELECT id, username, email FROM users WHERE id = $1"
row := pool.QueryRow(ctx, query, id)
// ...
}
```
### Solution 2: Parameterized Dynamic Query Builder
When building dynamic search filters, only append **static query clauses with indexed placeholders (`$%d`)**, while accumulating parameter values in a typed argument slice:
```go
// COMPLIANT: Dynamic query builder with parameterized placeholders
func SearchUsers(ctx context.Context, pool *pgxpool.Pool, filter UserFilter) ([]User, error) {
query := "SELECT id, username, email FROM users WHERE 1=1"
var args []any
idx := 1
if filter.Role != "" {
query += fmt.Sprintf(" AND role = $%d", idx)
args = append(args, filter.Role)
idx++
}
if filter.Status != "" {
query += fmt.Sprintf(" AND status = $%d", idx)
args = append(args, filter.Status)
idx++
}
rows, err := pool.Query(ctx, query, args...)
// ...
}
```
### Solution 3: Dynamic Table / Column Identifiers via Sanitizer
If table or column names must be selected dynamically, they cannot use `$n` placeholders. You **must** sanitize them using `pgx.Identifier`:
```go
// COMPLIANT: Dynamic identifier safely quoted and escaped
func GetTableCount(ctx context.Context, pool *pgxpool.Pool, tableName string) (int64, error) {
// Validates and safely double-quotes the identifier
safeTable := pgx.Identifier{tableName}.Sanitize()
query := "SELECT COUNT(*) FROM " + safeTable
var count int64
err := pool.QueryRow(ctx, query).Scan(&count)
return count, err
}
```
---
## 7. How to Suppress (Ignore Directives)
If you encounter an isolated legacy script or verified internal tool where dynamic query construction is proven safe, you can suppress the warning with a mandatory reason consisting of **at least 2 words**:
```go
// argus:ignore ARGUS-A01 internal bootstrap migration script
rows, err := dbpool.Query(ctx, rawDynamicQuery)
```
Alternatively, use the identifier alias:
```go
// argus:ignore UNSAFE_SQL_CONCATENATION verified internal maintenance query
rows, err := dbpool.Query(ctx, rawDynamicQuery)
```
---
## 8. Configuration Reference (`.argus.yaml`)
You can toggle or configure this rule globally in `.argus.yaml`:
```yaml
rules:
ARGUS-A01:
enabled: true
```