The Captcha Was Just Decoration: Two Government Portal Bugs I Reported to CERT-In
October 6, 2026 · 8 min read
Every age builds its walls out of whatever it knows best. The Romans used stone, medieval towns used timber and prayer, and we use code. History suggests walls are rarely brought down by armies. Usually someone just left the gate open because nobody thought to check it.
I'm not a security researcher. I'm a fullstack developer who spends most of his day in coding, and I teach Aptitude to kids in government schools through an NGO I work with. So when I poked at a couple of Chhattisgarh education portals this summer, it wasn't a bug bounty hunt. These were sites my students and their teachers actually use, and I wanted to see how they were built.
Turns out, not great. Over about a month I found two sets of issues, reported both to CERT-In, and watched both get fixed. Nothing here is a clever zero-day. That's the point of writing this. Both bugs are the kind every one of us has seen in code review, and probably shipped at least once.
This post covers what I found, the root causes, how the disclosure actually went with CERT-In, and what I'd tell any team shipping public-facing forms. Both issues are patched, so I'll talk about the mechanics, but I'm leaving out anything that reads like a recipe.
Bug 1: a captcha that never left the browser
The first one was on the NMMSE (National Means-cum-Merit Scholarship) results portal run by the state board. Classic ASP.NET WebForms page: enter a roll number, solve a captcha, get the result with the student's name, parents' names and school.
I opened devtools out of habit and noticed the two captcha inputs, the generated value and the user's answer, had no name attribute. If an input has no name, the browser doesn't include it in the form POST. So the server never saw the captcha at all. The "validation" was a JavaScript check that compared two fields on the client and then let the form submit.
In other words, the captcha was decoration. Anything that could POST a roll number plus the usual WebForms state fields to the print page got the result back, no puzzle required, no rate limit either. Roll numbers aren't secrets, so you can see where that goes: a scraper could pull PII for every scholarship candidate in an afternoon, and hammer the server while doing it.
There's an old pattern here. Most institutions are protected less by their locks than by the assumption that nobody will try the handle. That works right up until somebody does.
The fix is boring, which is how you know it's the right one. Generate the captcha server-side, keep it in session, validate it on POST before rendering anything, and put an IP rate limit in front of the endpoint. I put exactly that in my report.
Timeline:
| Date | What happened |
|---|---|
| 20 Jun 2026 | Reported to CERT-In, cc'd state NIC and the board |
| 20 Jun 2026 | CERT-In registered it (Ref: CERTIn-58373526), same morning |
| 19 Jul 2026 | CERT-In said the owner had fixed it; I re-tested and confirmed |
| 28 Jul 2026 | CERT-In sent a formal acknowledgement of responsible disclosure |
About five weeks from report to fix, on a government system. Honestly faster than some internal tickets I've filed.
Bug 2: 5,922 people's contact details, no login needed
A month later I was looking at a SCERT Chhattisgarh portal (the state council that trains teachers), and this one was a lot worse. I ended up writing a proper report with 16 findings: 4 critical, 5 high, 4 medium, 3 low.
The headline issue was a textbook IDOR. A set of report pages took a numeric subject ID in the query string, with no auth check at all, and returned the full list of subject experts for that subject: name, mobile number, personal email, district, institute. The IDs were sequential, roughly 1 to 19. Walk them and you get about 5,922 records across 25 institutes and 27+ districts. That's a ready-made phishing list of government teachers, sitting on public URLs.
I want to be clear that these weren't abstractions to me. Those were the phone numbers of teachers who had trusted a government form with their details and assumed, as we all assume, that someone had locked the cabinet. In the wrong hands that list is a harvest. Just imagine the fake calls from "the department" that could follow.
The other criticals stacked on top of it:
- Test credentials printed on a login page. A test account's username and password were rendered in plain text on the page, and they worked. They logged you into an admin report area.
- Full stack traces on bad input. Send a malformed ViewState and you got the whole ASP.NET yellow screen: physical paths, hosting provider, framework versions, compiled file names.
- No HTTPS. At all. Port 443 wasn't even configured, so every login and session cookie went over the wire in cleartext.
And then the usual long tail: an empty ViewState with valid credentials still produced a successful login, no anti-CSRF field, zero security headers but plenty of version-leaking ones, a session cookie without Secure, three jQuery versions loading at once (so the oldest, with known CVEs, wins), and no SRI on CDN scripts.
For the report I graded every finding, wrote a fix for each, and redacted every personal record in the evidence. I deliberately kept the exact reproduction steps for the login bypass out of the written PDF and offered to share them privately with whoever was doing the fix. CERT-In asked for a timestamped PoC, which I put together. The site has since been fixed, and I've gone back and confirmed it myself.
How the disclosure actually went
If you've never reported something to CERT-In, it's less scary than it sounds. You email incident@cert-in.org.in (there's also vdisclose@cert-in.org.in for vulnerability reports), they reply with a ticket number, and they chase the owning department for you. All their mail is PGP-signed, which is a nice touch.
Plato tells the story of Gyges, a shepherd who finds a ring that makes him invisible, and asks whether anyone so armed would stay honest. The internet hands that ring to anyone patient enough to open devtools. The temptation usually isn't to steal. It's to tweet a screenshot and be seen being clever. The boring path is better, and it's also the one that actually gets things fixed.
A few things I learned:
- Go to CERT-In, not just the site owner. Half the department addresses I cc'd bounced. One was a dead NIC address, another a board inbox that couldn't verify users. Without CERT-In in the loop, the reports would've gone nowhere.
- Write it like a bug ticket, not a blog post. Root cause, impact, affected URLs and fields, recommended fix. The person reading it is probably an overloaded contractor. Make their job easy.
- Bring a PoC, but keep it private. CERT-In will ask for a step-by-step video or screenshots with a visible timestamp. Have one ready. Keep the actual exploit steps out of anything that might get forwarded around.
- Touch as little as possible. I stayed unauthenticated, didn't brute-force anything, and didn't keep any of the data. Confirm the bug, scope the impact, stop.
- Re-test and say so. When they tell you it's fixed, check it yourself and reply. That's what closes the loop, and it's when I asked for the acknowledgement letter.
What I'd tell any team shipping public forms
None of this needed a security specialist to catch. It needed someone to ask "what does the server actually check?" Some questions worth putting in your review checklist:
- Does every control the UI enforces also exist on the server? If you deleted all your JavaScript, would the endpoint still refuse bad input?
- Can I change an ID in the URL and see someone else's data? If the answer is "only if you guess it," and the IDs are sequential, the answer is yes.
- Are there test accounts, debug pages or verbose errors in prod? Grep the rendered HTML, not just the repo.
- Is it HTTPS everywhere, with
Securecookies and HSTS? In 2026 this shouldn't be a finding, but here we are. - Who gets an email when someone finds a bug? Have a working security contact, ideally a
security.txt. Half my cc's bounced.
The thing that stuck with me is how ordinary these bugs were. No fancy chain, no novel technique. Just missing server-side checks on systems holding real people's data, the same thing we flag in PRs every week.
The odd fate of the defender is that when you succeed, nothing happens, and nothing is what you get thanked for. Thousands of students and teachers will never know their details were once sitting on a public URL. That's fine. It's the job working as intended.
India is building public digital infrastructure faster than almost anyone ever has, mostly with small teams on tight deadlines. Something that size can't be guarded by its builders alone. Civilisation, as history keeps reminding us, is held together less by laws than by habits, by lots of ordinary people doing the dull right thing when nobody would know if they didn't. In our corner of the world, that habit looks like noticing a missing check, filing a clear report at 1 a.m., and having the patience to see it through.
If you've found something similar and aren't sure how to report it, feel free to reach out. Happy to share my report template.
Aditya Raj Panjiyara (Adi) · hey-adi.me · LinkedIn