-
Notifications
You must be signed in to change notification settings - Fork 0
ARGUS A06
Rule Code:
ARGUS-A06Identifier:RUNTIME_DDLSeverity:CRITICAL(Catalog Lock Outage & Cluster Freeze Blocker) Category:Security & Operational SafetyTarget Standards: CWE-250 (Execution with Unnecessary Privileges), OWASP ASVS v4.0.3/v5.0 §V4.1.3
Application runtime code is strictly prohibited from executing Data Definition Language (DDL) commands such as CREATE, ALTER, DROP, TRUNCATE, RENAME, GRANT, REVOKE, COMMENT, or anonymous procedural blocks (DO $$ ... $$).
Database schema evolution must be isolated exclusively within dedicated migration scripts (db/migrations/) executed during deployment by a privileged migrator role. The runtime application database role must be restricted strictly to Data Manipulation Language (DML: SELECT, INSERT, UPDATE, DELETE).
Executing DDL in runtime code during live application traffic is the single most frequent cause of catastrophic database cluster outages:
-
Catalog & Relation Locking: Every DDL operation (e.g.
CREATE TABLE,ALTER TABLE,TRUNCATE) acquires anACCESS EXCLUSIVElock on target relations and updates system catalogs (pg_class,pg_attribute,pg_type). -
Lock Convoy: An
ACCESS EXCLUSIVElock blocks all other operations without exception-including lightweight read queries (SELECT). Hundreds of concurrent web requests queue up behind the lock, exhausting connection pools (pgxpool.Pool) within seconds.
In PostgreSQL 18.x, DDL operations broadcast shared invalidation messages via sinval.c. This forces every active backend process to discard and re-parse its prepared statement cache (plancache.c), triggering 100% CPU spikes across the database cluster.
flowchart TD
subgraph OUTAGE ["Runtime DDL Disaster Reaction (BLOCKED)"]
direction TB
App1["Go Web Service"] -->|"Executes CREATE TABLE / TRUNCATE"| PG["PostgreSQL Backend"]
PG -->|"Acquires ACCESS EXCLUSIVE Lock"| Catalog["Locks System Catalogs (pg_class, pg_type)"]
Catalog --> Convoy["Lock Convoy: All SELECT & INSERT Queries Blocked"]
Convoy --> PoolOut["pgxpool Connection Pool Exhausted"]
Convoy --> CacheDrop["sinval.c Flushes Prepared Plan Caches"]
PoolOut --> Outage["Cluster Freeze & HTTP 503 Total Outage (CWE-250)"]
end
subgraph ISOLATED ["Separation of Concerns (COMPLIANT)"]
direction TB
Migrator["CI/CD Migration Pipeline"] -->|"Runs Isolated .up.sql"| MigDB["Schema Updated in Pre-Deployment"]
RuntimeApp["Go Runtime App"] -->|"Executes DML Only (SELECT/INSERT)"| RunDB["Unblocked High-Concurrency Throughput (SAFE)"]
end
Creating temporary tables (CREATE TEMP TABLE) during request handling is frequently proposed for batch data staging. However, temporary tables still write to PostgreSQL system catalogs and induce catalog table bloat and lock contention. Runtime batch processing must utilize Go slice arrays with UNNEST($1::type[]), Common Table Expressions (WITH ...), or static staging tables defined in migrations.
Argus inspects all Go application call sites outside test files:
flowchart LR
Callsite["Extract DB Call<br/>(Query, Exec, SendBatch)"] --> Literal{"Is Query a Literal<br/>or Static String?"}
Literal -->|Yes| ASTCheck["ddl_nodes.go:<br/>pg_query_go AST Inspection"]
Literal -->|No| DynamicCheck["dynamic_ddl.go:<br/>fmt.Sprintf & Verbs Regex"]
ASTCheck --> NodeMatch{"Contains DDL Node?<br/>(CREATE, DROP, ALTER,<br/>TRUNCATE, GRANT, etc.)"}
DynamicCheck --> DynamicMatch{"Matches DDL Keywords?<br/>(CREATE TABLE, DROP, etc.)"}
NodeMatch -->|Yes| Report["Report CRITICAL Violation:<br/>Forbidden Runtime DDL"]
DynamicMatch -->|Yes| Report
NodeMatch -->|No| Safe["Pass (Valid DML Operation)"]
DynamicMatch -->|No| Safe
-
Comprehensive DDL Node Registry (
ddl_nodes.go): Traverses all statements in multi-statement queries (SELECT 1; DROP TABLE users;) and checks for all PostgreSQL DDL node types:-
CreateStmt,DropStmt,AlterTableStmt,TruncateStmt -
GrantStmt,GrantRoleStmt,IndexStmt,RenameStmt,CommentStmt -
CreateSeqStmt,AlterSeqStmt,CreateSchemaStmt,CreateExtensionStmt,DoStmt
-
-
Dynamic DDL Matcher (
dynamic_ddl.go): Detects dynamic format calls (fmt.Sprintf("CREATE TABLE %s ...")) and traced variable assignments. -
Exemptions: Automatic exemption for
_test.gofiles and migration runner packages.
| Failure Mode | Technical Impact | Risk Severity |
|---|---|---|
| Catalog Lock Convoy |
ACCESS EXCLUSIVE lock stalls all application reads and writes, crashing connection pools. |
CRITICAL |
| Plan Cache Thrashing | Shared invalidation messages flush prepared statement caches across all backend workers. | CRITICAL |
| Privilege Escalation Risk (CWE-250) | Granting DDL privileges to runtime app roles enables attackers to drop tables via SQLi. | CRITICAL |
| Catalog Bloat via Temp Tables | High-frequency CREATE TEMP TABLE leads to system catalog bloat and autovacuum spikes. |
HIGH |
// VIOLATION: Creating tables inside request handler
func ProvisionTenant(ctx context.Context, pool *pgxpool.Pool, tenantID string) error {
// Flagged: forbidden DDL statement (CREATE TABLE)
query := fmt.Sprintf("CREATE TABLE tenant_%s (id int, data text)", tenantID)
_, err := pool.Exec(ctx, query)
return err
}// VIOLATION: Using TRUNCATE inside runtime application service
func FlushAuthTokenCache(ctx context.Context, pool *pgxpool.Pool) error {
// Flagged: forbidden DDL statement (TRUNCATE)
_, err := pool.Exec(ctx, "TRUNCATE TABLE cached_tokens")
return err
}// VIOLATION: Concealed DDL inside multi-statement query
func ExecuteBatchCleanup(ctx context.Context, pool *pgxpool.Pool) error {
// Flagged: forbidden DDL statement (DROP)
_, err := pool.Exec(ctx, "SELECT 1; DROP TABLE legacy_sessions;")
return err
}// COMPLIANT: Runtime operations strictly use DML
func DeleteExpiredTokens(ctx context.Context, pool *pgxpool.Pool) error {
const query = "DELETE FROM cached_tokens WHERE expires_at < NOW()"
_, err := pool.Exec(ctx, query)
return err
}-- COMPLIANT: Placed in db/migrations/000042_add_tenant_tables.up.sql
CREATE TABLE IF NOT EXISTS tenants (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);// COMPLIANT: Use PostgreSQL UNNEST instead of CREATE TEMP TABLE
func BatchInsertUsers(ctx context.Context, pool *pgxpool.Pool, ids []int, names []string) error {
const query = `
INSERT INTO users (id, name)
SELECT * FROM UNNEST($1::int[], $2::text[])
`
_, err := pool.Exec(ctx, query, ids, names)
return err
}For test harnesses, benchmark harnesses, or disaster recovery tools:
// argus:ignore ARGUS-A06 ephemeral test database fixture setup
_, err := pool.Exec(ctx, "CREATE TABLE test_fixture (id int)")Alternatively, use the identifier alias:
// argus:ignore RUNTIME_DDL dedicated schema migration driver utility
_, err := pool.Exec(ctx, ddlMigrationStatement)Enable or configure this rule in .argus.yaml:
rules:
ARGUS-A06:
enabled: true