October 13, 2026
$11,500 for a Payload That Waited: Second-Order SQL Injection in an Admin Report
First-order SQL injection shows up immediately β you send a payload, the vulnerable query runs right then, and you see the effect in theβ¦

By T4nv1
3 min read
First-order SQL injection shows up immediately β you send a payload, the vulnerable query runs right then, and you see the effect in the same response. Second-order injection is slower and easier to miss entirely, because the payload gets stored safely, through a perfectly well-parameterized query, and only becomes dangerous much later, when some entirely different piece of code reads that stored value back out and uses it unsafely in a second query the original input never touched directly.
The target, which I'll call Quillmark here per their program's terms, is a team-collaboration platform where I'd already confirmed the registration and profile-update endpoints were solidly parameterized β direct injection attempts against the username field during registration failed cleanly, consistent with proper, safe query construction.
Why I Kept Looking
Standard injection testing against a field stopping at "the insert is safe" is reasonable but incomplete, because a value's first write isn't necessarily its only use. I registered a test account with a username deliberately containing a SQL metacharacter that would be completely inert in a parameterized insert but potentially dangerous if ever concatenated unsafely somewhere downstream:
username: tester'||(SELECT CASE WHEN (1=1) THEN pg_sleep(5) ELSE pg_sleep(0) END)||'username: tester'||(SELECT CASE WHEN (1=1) THEN pg_sleep(5) ELSE pg_sleep(0) END)||'Registration succeeded without incident β confirming, as expected, that the registration insert itself was safely parameterized and treated my username as inert data, storing the literal string exactly as submitted rather than executing any part of it.
Finding Where the Value Got Read Back Unsafely
Quillmark's admin dashboard included an activity report feature, generating a summary of recent user actions filterable by username β a secondary, administrative feature likely built separately, probably later, by a different part of the engineering effort than the core registration flow, and a reasonable place to suspect less rigorous query construction given its narrower, internal-facing audience.
I didn't have admin access myself, so confirming this required an indirect approach: triggering the report-generation logic through any path a lower-privileged test setup afforded, and watching for a timing difference consistent with my embedded pg_sleep payload actually executing when my stored username was later read back and used in a report query. Through a scheduled digest feature that referenced recent usernames in a similar report-style context and was reachable without admin privileges, I found a delayed response consistent with the conditional sleep firing β a response that took roughly five seconds longer than a baseline request, matching my payload's pg_sleep(5) exactly, when the condition evaluated true, and returning at normal speed when I adjusted the payload's condition to evaluate false.
Confirming the Root Cause, Not Just the Symptom
A timing difference alone is suggestive but not conclusive β I ran the true-condition and false-condition variants several times each to confirm the delay was consistent and reproducible rather than ordinary network jitter, and tested a boolean-based variant to extract a single known fact about the database (confirming the database version string's first character matched a specific value I could verify independently) as cleaner, unambiguous confirmation that real SQL execution was occurring against my stored username value, not just an unrelated processing delay.
Scope
All testing used a username I'd registered specifically for this research, and I limited extraction to a single, already-knowable fact about the database version rather than attempting to enumerate any real data.
The Report
Title: Second-order SQL injection via stored username value unsafely concatenated in downstream reporting query
Root cause: The username field is safely parameterized at registration, but a separate reporting feature later reads the stored username value and concatenates it directly into a SQL query without parameterization, allowing a payload stored safely at write-time to execute when read back in this different context.
Reproduction: The registration payload, the downstream reporting feature where it later executed, and timing-based and boolean-based confirmation of genuine SQL execution β limited to extracting a single already-known database version fact.
Impact: Full SQL injection capability against the application's database, reachable indirectly through any stored value that eventually flows into the vulnerable reporting query, without needing direct injection against any single immediately-exploitable endpoint.
Fix recommendations:
- Parameterize the reporting query exactly as rigorously as the registration insert; the safety of a value's first write provides no guarantee about its later reads
- Audit every downstream feature that reads previously stored user-supplied values for the same unsafe-concatenation pattern, since this class of bug specifically hides in secondary, less-scrutinized code paths
What Happened After
Quillmark parameterized the reporting query within a week and audited other admin-facing features reading stored user data, finding one additional instance. The report closed at $11,500, rated critical.
Why Second-Order Injection Rewards Following Data, Not Just Input Fields
This bug class is invisible to testing that only checks whether a field's own endpoint is vulnerable β the actual vulnerability lives wherever that stored value gets read back out later, potentially in a completely different feature built by different people at a different time. Worth tracing where every safely-stored value eventually gets reused, not just confirming its initial write is safe.