A ukey token in a forgot-password email handed me full read access to every record in their database. Reported in early 2025. Still live.
Press enter or click to view image in full size
I had their entire database in my terminal output.
Every table. Every user record. Every password hash stored since the application went live — sitting in plaintext SQLMap blocks, scrolling faster than I could read.
The endpoint that gave me this was one I had nearly dismissed. A ukey parameter appended to a password reset link. Sent to me by the application itself, in an email, as part of its normal forgot-password flow.
I had spent two days testing everything else on this site. Login form. Search. Profile fields. GET parameters. POST bodies. I had run the full sweep. Found nothing.
It was in the one link I almost didn’t click.
The site had all the signatures. Wappalyzer confirmed MariaDB. The UI looked like it hadn’t been touched since 2014 — misaligned div containers, unsuppressed server warnings, error messages that came back in raw PHP output without any sanitization. Old sites with old codebases tend to have old assumptions about security. One of the oldest is that string concatenation in SQL queries is fine.
I was confident this site was vulnerable. I just could not find where.
I went through every visible input field I could find. Classic payloads — ', ' OR 1=1--, "OR"1"="1, blind time-based delays. I ran SQLMap against every parameter that accepted user input. I tested GET parameters in the URL and POST bodies in Burp Suite. Login form. Search bar. Profile page. Contact form.
Nothing.
Not one error. Not one delay. Not one response that suggested the database was parsing my input differently. After two days of manual testing and an afternoon of automation, I was starting to think the Wappalyzer result was a false positive.
I almost moved on.
I came back to the site one more time. No reason, really — just the kind of stubbornness that makes you keep a browser tab open for a week. I had not tested the forgot password flow yet. Not because I thought it was safe, but because I had not thought about it at all.
I opened the form and typed in a random Gmail address — not an email from the site’s own domain. Then submitted.
The page returned a PHP warning. Then a second PHP warning. Then a fatal error:
Warning: Trying to access array offset on null in /home/***/forgetpassword.php on line 15
Warning: Trying to access array offset on null in /home/***/forgetpassword.php on line 16
Fatal error: Uncaught mysqli_sql_exception: You have an error in your SQL syntax;
check the manual that corresponds to your MariaDB server version for the right syntax
to use near '$2y$10$...xU9AQ/EMry9Onw8OYNXh1ADng1prgi',1)' at line 1
in /home/***/forgetpassword.php:21The SQL query was being built around my input with string concatenation — no prepared statements, no parameterization. The forgot password function expected an email from the site’s own domain. When it received something else, the query broke and the full error surfaced.
That was a finding by itself. But I had not found what I came for.
I registered an account using a valid domain email, triggered the forgot password flow properly, and checked my inbox. The reset link arrived a few seconds later:
https://helloxyz.com/forgetpassword.php?ukey=<hash>I read the URL. I read it again.
ukey was a bcrypt hash. $2y$10$ — I could see the beginning of it before the path was cut off. The server was receiving this value in a GET parameter and using it to look up the user session.
I opened Burp Repeater and added a single quote to the end of the hash.
ukey Token — Full Database, One PayloadThe server response came back immediately:
Fatal error: Uncaught mysqli_sql_exception: You have an error in your SQL syntax...
forgetpassword.php on line 21The same error. Same line. The ukey parameter was hitting the database raw — no sanitization, no parameterized query, nothing between my modified value and the SQL interpreter.
Join Medium for free to get updates from this writer.
I pointed SQLMap at the endpoint:
sqlmap -u "https://helloxyz.com/forgetpassword.php?ukey=HASH" \
--level=5 --risk=3 --dbs --batchUnion-based injection. Error-based injection. Both techniques confirmed. Within a few minutes, SQLMap had listed every database on the server. I ran the table enumeration. Then the column dump.
User records. Admin credentials. Email addresses. Session tokens. Password hashes. Everything that had been stored since the application went live — fully readable, no authentication required beyond having the URL from a reset email.
I attempted to escalate. File read via LOAD_FILE(). OS command execution via INTO OUTFILE and UDF injection. None of the escalation paths opened — the MySQL user running the queries did not have file or super privileges. RCE was off the table.
But I had the entire database. Full read access. That was more than enough to report critical.
Here is the thing about a ukey parameter in a password reset link: nobody thinks of it as user-controlled input.
The value looks like it was generated by the server. It looks cryptographic. It looks like something the application created and is simply consuming back on the other end — an internal handshake that the user is just passing along. Nobody typed that value. Nobody edits it. Every bug hunter who ever tested this site probably saw that hash in the URL and moved on without touching it.
That assumption is exactly the problem.
The ukey parameter arrives in a GET request to the server. The server has no way to know whether you modified it or not. It receives the string, drops it into a SQL query, and executes. If the developer who wrote forgetpassword.php made the same assumption — that this value would never be tampered with — they would have seen no reason to sanitize it.
The parameter that looks server-controlled is still user-controlled. It always is.
This is also why it survived every automated scan. SQLMap and most scanners crawl what they can reach through a browser session — visible links, form inputs, standard flows. A password reset link delivered to an email inbox is not in the crawl path. No scanner touches it by default. No automated tool registers it as an input. It requires a human to notice the URL, read the parameter, and choose to test it.
Most people do not make that choice. I almost did not.
I filed the report in early 2025. Full reproduction steps. SQLMap output. Screenshot of the fatal error. Parameter name, endpoint path, confirmed injection type.
I checked the endpoint again while writing this. The forgot password form still throws the same error when you enter a non-domain email. The ukey parameter is still injectable. The database is still readable. Nothing has changed in sixteen months.
I do not write that with satisfaction. I write it because it is the most instructive part of this finding — not the technical detail, but what happens after.
A SQL injection vulnerability in a password reset flow is not a borderline finding. It is not ambiguous severity. Full database read access with a single parameter modification is as clear a critical as you can find. The remediation is one line of code — switch mysqli_query() to a prepared statement. Any PHP developer with a week of experience knows how to do it.
And yet the vulnerability sits there. Open. Reproducible. Every user who creates an account on that site has their credentials sitting in a database that is readable through a public endpoint.
I do not know why some reports disappear into review and never come out. I have seen it enough times now to suspect it is not about the severity — it is about the internal cost of acknowledging a vulnerability publicly versus the perceived risk of leaving it in place quietly. For a small site with no active security team, the path of least resistance is inaction.
That does not make it acceptable. It makes it a pattern worth naming.
If you are testing a web application and you have exhausted every visible input field and found nothing, there is one place left to check before you close the tab.
Trigger the forgot password flow. Use an email you control. Open the link they send you.
Read the URL. Every parameter. The token is not just a credential for authentication — it is also an input to a SQL query. The developer who wrote the reset flow was thinking about account recovery, not injection prevention. That mismatch is where vulnerabilities live.
Test the token. Add a quote. Add a comment character. Run SQLMap with the full URL and the parameter flagged explicitly. Most of the time, nothing happens. But sometimes, the entire database comes back.
The parameter that looks the least like an attack surface is exactly where you should look.
Did your scanner ever touch this parameter? Or was the reset link outside its crawl path entirely? Drop your experience in the comments — I want to know how others approach this.