# ARGUS-A04: Unsafe Dynamic ORDER BY Clause
> **Rule Code:** `ARGUS-A04`
> **Identifier:** `UNSAFE_ORDER_BY`
> **Severity:** `HIGH` (Inferential Blind SQLi & Information Leakage Blocker)
> **Category:** `Security & Data Integrity`
> **Target Standards:** CWE-89 (SQL Injection via Order By), OWASP ASVS v4.0.3/v5.0 §V5.3.1
---
## 1. Overview & Core Invariant
Dynamic SQL identifiers within the **`ORDER BY`** clause (column names) and sort directions (`ASC` / `DESC`) **must be validated and mapped exclusively via compile-time closed-set allowlist maps** (`map[string]string`) or static `switch-case` branches with default fallbacks.
Interpolating raw user input directly into `ORDER BY` is strictly forbidden. Crucially, simple identifier quoting-such as `pgx.Identifier{userInput}.Sanitize()`-is **insufficient and prohibited** because quoting prevents syntax-breaking attacks but does not prevent attackers from ordering results by unauthorized private columns (e.g. `password_hash`, `totp_secret`, `deleted_at`).
---
## 2. Technical Grounding & PostgreSQL Engine Realities
### 2.1. Why Bind Parameters ($1) Do Not Work in ORDER BY
In PostgreSQL Extended Query Protocol v3.0, parameter placeholders (`$1, $2, ...`) cannot represent SQL column identifiers:
- If a developer executes `SELECT id FROM users ORDER BY $1`, PostgreSQL evaluates `$1` as a scalar constant literal for every tuple.
- The query executes as a **no-op sort**, leaving rows in non-deterministic physical order.
- This protocol limitation tempts developers into dynamic string formatting (`fmt.Sprintf`), exposing applications to inferential SQL injection.
### 2.2. Inferential Blind SQL Injection (CWE-89)
Because `ORDER BY` accepts arbitrary SQL expressions, an attacker can extract private data bit-by-bit by injecting conditional expressions:
```
sort=(CASE WHEN (SELECT ASCII(SUBSTRING(password_hash, 1, 1))
FROM admin_users WHERE id = 1) = 97
THEN created_at ELSE id END)
```
PostgreSQL evaluates the subquery for every request:
- If `True`: The result set is ordered by `created_at`.
- If `False`: The result set is ordered by `id`.
By observing the order of returned rows, the attacker reconstructs entire confidential tables without triggering any database errors or audit logs.
```mermaid
flowchart TD
subgraph SAFE ["Closed-Set Allowlist Map (SAFE)"]
direction TB
Input1["Client: sort=date"] --> Map1["sortAllowlist['date']"]
Map1 -->|"Known Static Identifier"| Query1["ORDER BY created_at DESC"]
Query1 --> Plan1["PostgreSQL: B-Tree Index Scan (FAST)"]
end
subgraph INJECTION ["Dynamic Interpolation / Quoting (BLIND SQLi RISK)"]
direction TB
Input2["Client: sort=(CASE WHEN ...)"] --> Concat2["fmt.Sprintf: Injects Raw Input"]
Concat2 --> Parse2["PostgreSQL Evaluates Subquery Expression"]
Parse2 --> Exfil2["Blind Data Exfiltration & Disk Spill (CWE-89)"]
end
```
### 2.3. Performance Impact in PostgreSQL 18
Arbitrary or expression-based sort targets disable modern PostgreSQL 18 optimizations such as **B-tree Index Skip Scan** and **Incremental Sort**. PostgreSQL is forced to allocate a memory-intensive _Sort Node_ inside `work_mem` or spill intermediate sort runs to temporary disk files, drastically increasing query latency.
---
## 3. How Argus Detects Violations (Static Analysis Architecture)
Argus combines Go AST data flow analysis with SQL SortClause inspection:
```mermaid
flowchart LR
Call["Detect fmt.Sprintf
with ORDER BY"] --> Extract["Isolate Placeholders
in ORDER BY Clause"]
Extract --> QuotingCheck{"Is Argument Wrapped in
pgx.Identifier.Sanitize()?"}
QuotingCheck -->|Yes| ReportQuote["Report Violation:
Quoting Insufficient for ORDER BY"]
QuotingCheck -->|No| FlowCheck{"Does Variable Originate from
Map Index or Switch-Case?"}
FlowCheck -->|Yes| Pass["Pass (Safe Closed-Set Mapping)"]
FlowCheck -->|No| ReportUnsafe["Report Violation:
Unsafe Dynamic ORDER BY"]
```
1. **Clause Isolation:** Identifies `fmt.Sprintf` calls containing an `ORDER BY` clause and maps argument placeholder indices specifically residing inside the sorting clause.
2. **Quoting Rejection:** Detects calls to `pgx.Identifier.Sanitize()` or `SanitizeIdentifier()` and rejects them explicitly.
3. **Data Flow Validation:** Inspects local variable assignments (`*ast.AssignStmt`):
- Accepts map index lookups: `safeCol, ok := sortMap[userSort]`
- Accepts switch-case statements where each case branch assigns a static compile-time string literal.
- Accepts sort direction checks strictly bounded to `"ASC"` or `"DESC"`.
4. **PostgreSQL AST Parser:** Validates static and dynamic queries using `pg_query_go` to confirm that `SortClause` expressions do not contain complex, untrusted expression trees.
---
## 4. Vulnerability & Risk Taxonomy
| Failure Mode | Technical Impact | Risk Severity |
| :------------------------------- | :----------------------------------------------------------------------------------------------- | :------------ |
| **Direct Parameter Injection** | Inferential Blind SQL Injection (CWE-89) extracting sensitive credentials without syntax errors. | **CRITICAL** |
| **Identifier Quoting Bypass** | Access and sorting on private columns (`password_hash`, `totp_secret`, `deleted_at`). | **HIGH** |
| **Unvalidated Sort Direction** | Injection of arbitrary clauses via the sort direction parameter. | **HIGH** |
| **Arbitrary Expression Sorting** | Bypasses B-tree index sort paths, triggering unindexed disk spills and CPU spikes. | **MEDIUM** |
---
## 5. Non-Compliant Code Patterns (Bad Examples)
### Example 1: Direct Injection from HTTP Request
```go
// VIOLATION: Directly interpolating query parameter into ORDER BY
func ListUsers(ctx context.Context, pool *pgxpool.Pool, r *http.Request) ([]User, error) {
sortBy := r.URL.Query().Get("sort")
// Flagged: Unsafe dynamic ORDER BY expression!
query := fmt.Sprintf("SELECT id, name FROM users ORDER BY %s ASC", sortBy)
rows, err := pool.Query(ctx, query)
// ...
}
```
### Example 2: False Security via Quoting Sanitizer
```go
// VIOLATION: Relying on pgx.Identifier quoting for ORDER BY
func ListUsers(ctx context.Context, pool *pgxpool.Pool, r *http.Request) ([]User, error) {
sortBy := r.URL.Query().Get("sort")
safeCol := pgx.Identifier{sortBy}.Sanitize()
// Flagged: Quoting does NOT restrict sorting to public columns!
query := fmt.Sprintf("SELECT id, name FROM users ORDER BY %s ASC", safeCol)
rows, err := pool.Query(ctx, query)
// ...
}
```
### Example 3: Unvalidated Sort Direction
```go
// VIOLATION: Dynamic sort direction without strict ASC/DESC constraint
func ListOrders(ctx context.Context, pool *pgxpool.Pool, dir string) ([]Order, error) {
// Flagged: dir can be injected with arbitrary SQL expressions
query := fmt.Sprintf("SELECT id, total FROM orders ORDER BY created_at %s", dir)
rows, err := pool.Query(ctx, query)
// ...
}
```
---
## 6. Compliant Implementation Patterns (Good Examples)
### Solution 1: Closed-Set Allowlist Map (Standard)
```go
// COMPLIANT: Validated against a closed-set allowlist map with static fallback
var userSortAllowlist = map[string]string{
"name": "name",
"email": "email",
"date": "created_at",
}
func ListUsers(ctx context.Context, pool *pgxpool.Pool, r *http.Request) ([]User, error) {
sortBy := r.URL.Query().Get("sort")
column, ok := userSortAllowlist[sortBy]
if !ok {
column = "id" // Safe deterministic fallback
}
query := fmt.Sprintf("SELECT id, name, email FROM users ORDER BY %s ASC", column)
rows, err := pool.Query(ctx, query)
// ...
}
```
### Solution 2: Explicit Switch-Case Mapping
```go
// COMPLIANT: Every branch maps to a compile-time string literal
func GetSortColumn(sortBy string) string {
switch sortBy {
case "name":
return "name"
case "date":
return "created_at"
default:
return "id"
}
}
```
### Solution 3: Strict Direction Validation
```go
// COMPLIANT: Explicit validation for sort direction
direction := "ASC"
if strings.ToUpper(r.URL.Query().Get("dir")) == "DESC" {
direction = "DESC"
}
query := fmt.Sprintf("SELECT id, name FROM users ORDER BY created_at %s", direction)
```
---
## 7. How to Suppress (Ignore Directives)
For internal reporting scripts or batch utilities where dynamic ordering has been manually verified:
```go
// argus:ignore ARGUS-A04 internal reporting analytics query with trusted admin input
query := fmt.Sprintf("SELECT id, name FROM users ORDER BY %s ASC", userSort)
```
Alternatively, use the identifier alias:
```go
// argus:ignore UNSAFE_ORDER_BY verified batch exporter sort column
query := fmt.Sprintf("SELECT id, name FROM users ORDER BY %s ASC", userSort)
```
---
## 8. Configuration Reference (`.argus.yaml`)
Enable or configure this rule globally in `.argus.yaml`:
```yaml
rules:
ARGUS-A04:
enabled: true
```