System Context
Gavel is an internal auction-style application designed to manage inventory and bidding workflows. The system relied on PDO prepared statements and was assumed to be protected against SQL injection through parameterization.
User-facing features included dynamic sorting and filtering to support operational flexibility.
Failed Design Assumption
The core assumption was that prepared statements alone were sufficient to enforce query safety. However, user-controlled input influenced query structure during runtime, crossing the boundary between data and execution logic.
As a result, the database executed queries whose behavior was partially defined by external input rather than fixed application intent.
Impact
This design flaw enabled disclosure of sensitive credential data, providing initial access to the system. Once access was established, additional weaknesses could be chained to escalate privileges and compromise system integrity.
The failure was rooted in logic design, not missing patches or outdated dependencies.
Why This Was Hard to Detect
The application behaved correctly under normal usage. Query syntax remained valid, and no obvious injection patterns appeared during routine testing.
Because the behavior aligned with legitimate workflows, abnormal execution paths blended into expected activity, allowing the flaw to survive security review.
Design Lessons
This case demonstrates that:
- Prepared statements protect values, not execution intent
- Trust boundaries must be explicit and enforced structurally
- Security guarantees must not depend on runtime interpretation
When execution logic becomes flexible, security becomes fragile.
Related Research
Related Patterns
Technical Proof
→ Technical proof and source analysis available on request.
Vulnerability Analysis
The core failure was dynamic SQL construction that bypassed PDO's parameterization for ORDER BY and sort direction clauses. While PDO correctly parameterized WHERE filters, the sort column and direction were interpolated directly from user input into the query string.
-- Vulnerable pattern: user-controlled sort column injected raw
$query = "SELECT * FROM items WHERE auction_id = ? ORDER BY " .
$_GET['sort'] . " " . $_GET['dir'];
$stmt = $pdo->prepare($query);
$stmt->execute([$auctionId]);
The application assumed that because parameterization was in use for the WHERE clause, the query was safe. This is a trust boundary collapse: the developer trusted that PDO's protection extended to parts of the query it cannot protect.
Attack Chain
Step 1 — Identify the injection point
The /items endpoint accepted sort and dir GET parameters. A simple test confirmed the injection by appending a SLEEP(2) payload:
GET /items?auction_id=5&sort=price,(SELECT+SLEEP(2))--&dir=ASC
A 2-second delay confirmed blind injection via the ORDER BY clause.
Step 2 — Extract schema information
Using time-based blind injection to enumerate tables:
sort=price,(SELECT SLEEP(2) FROM information_schema.tables
WHERE table_name='admin_credentials' LIMIT 1)--
The 2-second response confirmed admin_credentials existed.
Step 3 — Credential disclosure
Column enumeration revealed username and password_hash fields. Time-based extraction of individual characters allowed full credential recovery.
Design Failure
The root cause is not SQL injection — it is a design assumption failure. The developer correctly knew about parameterization but incorrectly believed it applied to ORDER BY clauses. PDO cannot parameterize identifiers (column names, table names, sort directions). Only literals (values) can be parameterized.
The correct fix is an explicit allowlist:
$allowed_sorts = ['price', 'title', 'created_at', 'end_time'];
$allowed_dirs = ['ASC', 'DESC'];
$sort = in_array($_GET['sort'], $allowed_sorts) ? $_GET['sort'] : 'created_at';
$dir = in_array($_GET['dir'], $allowed_dirs) ? $_GET['dir'] : 'DESC';
$query = "SELECT * FROM items WHERE auction_id = ? ORDER BY {$sort} {$dir}";
MITRE ATT&CK Mapping
- T1190 — Exploitation of Public-Facing Application
- T1555 — Credentials from Password Stores
- T1005 — Data from Local System
Lessons
- Parameterization protects values, not identifiers.
ORDER BYcolumns must be validated against an allowlist. This is non-negotiable. - PDO does not protect you from yourself. Framework usage is not a substitute for understanding what the framework actually does.
- Blind injection is patient. Even without UNION-based extraction, a time-based approach recovers credentials given enough requests. Rate limiting alone does not prevent this.
- Trust boundaries must be explicit. Every point where user-controlled data enters a query must be documented and reviewed, regardless of the query builder used.