How an Expired Trial Led to a Critical Email Verification Bypass

During a recent pentest of a coworking space management application, our team had a really productive start. We quickly found some serious stuff: a critical IDOR that let us hijack other tenants' payment methods, and a stored XSS right in the admin dashboard. We also identified a few "low-hanging fruits" along the way. But after that initial success, the pace really slowed down, and It genuinely appeared we'd gotten to the bottom of all the significant vulnerabilities, so we shifted our focus to other projects.
A few days later, as I was about to write the final report, something in me said, "Give it one more shot." The only problem was, all our test accounts were trial accounts, and they had expired. So, to get back in, I had to create a new one.
I went through the usual signup process, entered my details, and then landed on the email verification page, asking for the verification code from my inbox. That's when I noticed the URL in the address bar. And there it was—a token parameter, looking exactly like a Base64 string, followed by a suffix. I couldn't believe I'd missed it before!
This immediately got my attention. It’s always a point of interest when an application puts a reversible string in the URL for a security-related flow. Why would any part of the verification secret be visible on the client-side?
Analyzing the Token
The process to confirm this suspicion was straightforward. The URL in question had the following structure:
https://[REDACTED_APP].com/email_confirmations/new?token=BASE64_TOKEN--<suffix>
I copied the BASE64_TOKEN
part and started decoding it layer by layer.
First, the outer wrapper revealed a framework fingerprint:
{
"_rails": {
"message": "ANOTHER_BASE64_STRING",
"exp": null,
"pur": "email_confirmation"
}
}
To a developer familiar with Ruby on Rails, this structure is instantly recognizable. It’s a serialized message generated by the framework, likely using ActiveSupport::MessageVerifier
. This tool is designed to create secure, signed tokens to prevent tampering—perfect for features like password reset links or email confirmations.
The irony here is that a feature designed for security was being used to deliver an insecure payload. The token itself might be tamper-proof, but that doesn't matter if the sensitive data is just sitting inside, encoded but not truly encrypted. The message
field contained the next layer.
Decoding this second string revealed the core issue:
{
"content": "{\"user_id\":\"xxxx...\",\"code\":\"198548\",...}",
"expires_at": "2025-07-18T08:53:33Z"
}
And inside, in plain text, was the verification code itself: "code":"198548"
. The application was sending the secret code to the user's email while simultaneously embedding it in the URL of the verification page. I copied the code, pasted it into the form, and the account was successfully verified without ever needing to access the email inbox.
The Security Risk
For an application managing coworking spaces, this is a serious risk. The trust that a member is who they claim to be is fundamental. This vulnerability allows an attacker to:
Impersonate members or staff: An attacker could register as
manager@thecoworkspace.com
or as an employee of a known company within the space, likedev@startup.com
, to lend legitimacy to other malicious activities.Access member-only resources: This could potentially lead to unauthorized booking of conference rooms, access to private community forums, or abuse of member-specific perks.
Commit fraud: An attacker could exploit new-member promotions or link their own payment details to a fake but verified account.
The Right Way to Handle Verification
The recommendation here is foundational. The server should never trust the client with secrets.
Use Opaque Tokens: The token in the URL should be a random, meaningless string (an opaque token like a UUIDv4). Its only purpose is to identify which verification attempt is in progress.
Keep Verification Logic Server-Side: The actual verification code should be stored securely on the server (hashed, ideally) and associated with the opaque token. The comparison of the user-submitted code and the correct code must only happen on the server.
Use Framework Features Correctly: Even when using strong features like Rails'
MessageVerifier
, remember that the tool is only as secure as the data you put inside it. Never place the secret answer (the code) inside the message that the user can decode.
The Takeaway
It's interesting how things work out. The most critical finding of this engagement didn't come from a complex attack chain. It came from a simple logistical hurdle—an expired trial account forced me to look closely at the most basic part of the application again. It serves as a great reminder that even in a mature testing process, you can’t overlook the fundamentals. Sometimes the most significant vulnerabilities are hiding in the simplest workflows.
The security of your application depends on scrutinizing every detail, from complex features to the most basic. At NullCore Labs, our comprehensive penetration testing services are designed to do just that. Find out how we can help secure your business at https://nullcorelabs.com/.