# ARGUS-A14: Forbidden Wildcard SELECT * Queries > **Rule Code:** `ARGUS-A14` > **Identifier:** `FORBIDDEN_SELECT_STAR` > **Severity:** `HIGH` (TOAST Table Bloat, Buffer Cache Pollution & PII Leak) > **Category:** `Performance, Memory Bounds & Security (Anti-Overfetching)` > **Target Standards:** CWE-200 (Exposure of Sensitive Information to an Unauthorized Actor), OWASP ASVS v4.0.3/v5.0 §V8.3.2, PostgreSQL Performance Guidelines --- ## 1. Overview & Core Invariant All SQL queries executed in production application code **must define explicit column projections**. The use of wildcard expressions (**`SELECT *`** or **`SELECT alias.*`**) is strictly forbidden. Every data retrieval query must list required column identifiers specifically (`SELECT id, name, status, created_at`). Legitimate exemptions are limited to: 1. Row counting aggregate functions: **`COUNT(*)`** or **`COUNT(DISTINCT *)`**. 2. Boolean existence subqueries: **`EXISTS (SELECT 1 ... / SELECT * ...)`** where the PostgreSQL query planner ignores projection cost. --- ## 2. Technical Grounding & PostgreSQL Engine Realities ### 2.1. TOAST Attribute Dereferencing & Buffer Cache Pollution PostgreSQL stores oversized attributes (`TEXT`, `JSONB`, `BYTEA`, arrays) out-of-line in separate TOAST tables. - Executing `SELECT *` forces PostgreSQL to perform random disk I/O seeks to dereference each TOAST pointer, even if the application never reads those fields. - TOAST data floods `shared_buffers`, evicting hot cache pages from RAM. ### 2.2. Nullification of Index-Only Scans When a query requests only columns contained in an index (e.g. `SELECT id, status FROM users WHERE status = 'active'`), PostgreSQL executes an **Index-Only Scan** without touching table heap pages (_zero table disk I/O_). Wildcard `SELECT *` breaks Index-Only Scans completely, forcing physical heap lookups for every matching row. ### 2.3. Sensitive Information Exposure (CWE-200 & OWASP §V8.3.2) When new private columns (e.g. `password_hash`, `totp_secret`, `ssn`) are added to database tables, API handlers querying via `SELECT *` automatically map and expose these fields into public JSON responses. ```mermaid flowchart TD subgraph OVERFETCH ["Wildcard SELECT * (HAZARDOUS)"] direction TB Q1["SELECT * FROM users WHERE id = $1"] --> TOAST["Dereferences 2MB JSONB/BYTEA from TOAST Table"] Q1 --> Heap["Forces Physical Table Heap Scan (No Index-Only Scan)"] Q1 --> PII["Accidentally Pulls Private Columns (password_hash, totp_secret)"] PII --> Leak["PII Leaked into Public API JSON Response (CWE-200)"] end subgraph EXPLICIT ["Explicit Column Projection (COMPLIANT)"] direction TB Q2["SELECT id, name, email FROM users WHERE id = $1"] --> FastScan["Enables Fast Index-Only Scan (Zero Heap I/O)"] Q2 --> Lean["Bypasses Unnecessary TOAST Table Seeks (95% Bandwidth Saved)"] Q2 --> Secure["Strictly Confines Data to Intended DTO Fields (Zero PII Leak)"] end ``` --- ## 3. How Argus Detects Violations (Static Analysis Architecture) Argus evaluates SQL queries across all database call sites using AST inspection: ```mermaid flowchart LR Scan["Scan DB Query Calls
(Exclude _test.go)"] --> Parse["ast_visitor.go:
pg_query_go AST Inspection"] Parse --> TargetList{"ResTarget Node Contains
ColumnRef AStar (* or alias.*)?"} TargetList -->|Yes| ExceptionCheck{"exceptions.go:
Inside COUNT(*) or
EXISTS(...) SubLink?"} TargetList -->|No| Subqueries{"Inspect CTEs, FromClause Subqueries,
and UNION/INTERSECT"} Subqueries -->|Contains Star| ExceptionCheck ExceptionCheck -->|Yes| Pass["Pass (Legitimate Exception)"] ExceptionCheck -->|No| Report["Report HIGH Violation:
Forbidden SELECT * Wildcard"] Subqueries -->|No Star| Pass ``` 1. **AST TargetList Inspection (`ast_visitor.go`):** Identifies `Node_AStar` within `ResTarget` and `ColumnRef` nodes. 2. **Subquery & CTE Traversal (`ast_visitor.go`):** Recursively inspects Common Table Expressions (`WithClause`), subselects (`FromClause`), and Set Operations (`Larg`/`Rarg`). 3. **Exemption Filtering (`exceptions.go`):** Allows `COUNT(*)` and boolean probe subqueries inside `EXISTS(...)`. --- ## 4. Vulnerability & Risk Taxonomy | Failure Mode | Technical Impact | Risk Severity | | :------------------------------- | :------------------------------------------------------------------------------------------- | :------------ | | **Accidental PII Exposure** | Newly added sensitive columns are automatically mapped and exposed via API responses. | **CRITICAL** | | **TOAST Table Flooding** | Heavy JSONB/BYTEA attributes saturate memory buffers and increase query latency. | **HIGH** | | **Index-Only Scan Invalidation** | Forces costly physical table heap access on queries that could run entirely in index memory. | **HIGH** | | **Positional Scan Panics** | Application runtimes crash if migration alters column sequence order. | **HIGH** | --- ## 5. Non-Compliant Code Patterns (Bad Examples) ### Example 1: Basic Wildcard Query ```go // VIOLATION: Selects all columns including potential TOAST attributes and private fields func GetUser(ctx context.Context, pool *pgxpool.Pool, userID int) (*User, error) { // Flagged: Forbidden 'SELECT *' or wildcard column selection const query = "SELECT * FROM users WHERE id = $1" var u User err := pool.QueryRow(ctx, query, userID).Scan(&u.ID, &u.Name, &u.Email) return &u, err } ``` ### Example 2: Table Alias Wildcard in JOIN ```go // VIOLATION: Alias wildcard over-fetches entire table func GetUserOrders(ctx context.Context, pool *pgxpool.Pool, userID int) ([]Order, error) { // Flagged: Forbidden 'SELECT *' or wildcard column selection const query = "SELECT u.*, o.id FROM users u JOIN orders o ON u.id = o.user_id" rows, err := pool.Query(ctx, query) return parseOrders(rows), err } ``` ### Example 3: Wildcard in Common Table Expression (CTE) ```go // VIOLATION: CTE wildcard over-fetches into temp working memory func GetActiveOrders(ctx context.Context, pool *pgxpool.Pool) error { // Flagged: Forbidden 'SELECT *' or wildcard column selection const query = `WITH active_users AS ( SELECT * FROM users WHERE status = 'active' ) SELECT id FROM active_users` _, err := pool.Exec(ctx, query) return err } ``` --- ## 6. Compliant Implementation Patterns (Good Examples) ### Solution 1: Explicit Column Projection ```go // COMPLIANT: Fetches only the required columns func GetUser(ctx context.Context, pool *pgxpool.Pool, userID int) (*User, error) { const query = "SELECT id, name, email, status FROM users WHERE id = $1" var u User err := pool.QueryRow(ctx, query, userID).Scan(&u.ID, &u.Name, &u.Email, &u.Status) return &u, err } ``` ### Solution 2: Aggregate Function `COUNT(*)` ```go // COMPLIANT: COUNT(*) aggregate row counting is explicitly permitted func CountActiveUsers(ctx context.Context, pool *pgxpool.Pool) (int64, error) { const query = "SELECT COUNT(*) FROM users WHERE status = 'active'" var count int64 err := pool.QueryRow(ctx, query).Scan(&count) return count, err } ``` ### Solution 3: Boolean `EXISTS` Subquery ```go // COMPLIANT: EXISTS subquery projection is optimized by PostgreSQL planner func UserHasOrders(ctx context.Context, pool *pgxpool.Pool, userID int) (bool, error) { const query = "SELECT EXISTS(SELECT 1 FROM orders WHERE user_id = $1)" var exists bool err := pool.QueryRow(ctx, query, userID).Scan(&exists) return exists, err } ``` --- ## 7. How to Suppress (Ignore Directives) For low-level disaster recovery database dump utilities or administrative schema introspection tools: ```go // argus:ignore ARGUS-A14 low-level database disaster recovery full row export rows, err := pool.Query(ctx, "SELECT * FROM audit_archive") ``` Alternatively, use the canonical identifier alias: ```go // argus:ignore FORBIDDEN_SELECT_STAR administrative schema exporter rows, err := pool.Query(ctx, dumpQuery) ``` --- ## 8. Configuration Reference (`.argus.yaml`) Enable or configure this rule in `.argus.yaml`: ```yaml rules: ARGUS-A14: enabled: true ```