After a year of pen-testing Web3 applications at Securze, we’ve seen patterns that most security discussions miss. While everyone focuses on smart contract vulnerabilities and consensus attacks, some of the most critical issues we’ve uncovered exist in places developers don’t expect.
Throughout 2025, our team has assessed security across the Web3 ecosystem: crypto wallets managing substantial assets, blockchain gaming platforms with real-money economies, trading exchanges processing high-volume transactions, and NFT marketplaces hosting valuable digital collections. Each engagement revealed something different, from critical flaws that could drain funds instantly, to logical vulnerabilities hiding in plain sight, to sophisticated attack vectors that required creative exploitation techniques.
Here’s what makes Web3 security fascinating: the vulnerabilities range dramatically. Some are brutally simple yet devastating in impact. Others are technically complex, buried deep in the architecture, demanding expertise to even identify. All of them, however, share one thing in common – they can cause serious damage to platforms and reputations.
Whether you’re a founder making strategic decisions or a technical leader architecting systems, understanding these vulnerabilities matters. Not theoretical ones from bug bounty write-ups, but real issues we’ve identified and helped remediate in production systems.
We’re sharing these stories, the actual vulnerabilities, the impact they could have caused, and the lessons worth remembering. No fluff, no manufactured scenarios. Just real findings from real assessments.
Let’s talk about what we found.
Vulnerability #1: Logic Flaws in Authentication – The Signup Shortcut
Application Type:
Web3 asset conversion platform enabling cryptocurrency trading and portfolio management.
Vulnerability Description & Severity:
Critical Severity – Multiple logic flaws in the signup and signin flows allowed attackers to bypass password authentication entirely and take over accounts through API manipulation and OTP brute-forcing.
Behind the Scenes (Discovery):
Sometimes the most dangerous vulnerabilities aren’t about breaking encryption or exploiting complex code – they’re about understanding how an application thinks its security works, and finding the gaps in that logic.
This platform had two separate modules: one for signing up new users, and one for signing in existing users. Both seemed secure on the surface. But here’s the thing about security: it’s not just about having locks on your doors – it’s about making sure those locks actually connect to something.
We started mapping out how the signup process worked, step by step. Then we did the same for the signin process. And that’s when we noticed something interesting: these two processes shared components, but they weren’t talking to each other properly. It was like having a front door with a deadbolt and a back door with no lock at all – but both doors leading into the same house.
Technical Flow:
Let’s walk through how this application actually worked, and then you’ll see exactly where the logic broke down.
How Signup Actually Worked:
When a user created a new account, they visited the signup page and entered their email address and password. Behind the scenes, the application made its first check: it queried the database to see if that email already existed. If the email was already registered, it returned an error message: “Email already exists.” If the email was new, the system saved it in the database and assigned that user a unique identifier – their user_id. This was a properly implemented UUID (a long alphanumeric string like a3d5f891-4c2e-4b8a-9d3f-8e7c1b2a4f6d), following security best practices.
Next came the verification step. The application sent a request to its OTP endpoint with the query parameter “signup” and the newly assigned user_id. The system looked up which email belonged to that user_id, generated a random 6-digit OTP code, and sent it via email. When the OTP was sent successfully, the system returned a confirmation_token in the response – a temporary identifier indicating that an OTP had just been sent for this user.
Finally, the user entered the OTP code from their email, and the account was fully activated.
How Signin Actually Worked:
For existing users logging back in, they entered their email and password on the signin page. The system verified the credentials against the database. If the password was correct, it redirected the user to an OTP verification page. The system sent a 6-digit code to their email, and once they entered it correctly, they were logged in.
The application used two-factor authentication – password plus OTP. On paper, this looked secure.
The Problems We Found:
Problem 1: The Signup OTP Endpoint Had No Context Validation
We discovered that the OTP endpoint with the signup query parameter had two critical flaws that, when combined, completely broke the authentication model.
Flaw 1: Signup OTPs worked for signin
The system had separate signup and signin flows, but they shared the same OTP verification mechanism. When we generated an OTP using POST /otp?signup, that OTP could be used at the signin verification endpoint. The system didn’t distinguish between “this OTP was generated for account creation” versus “this OTP was generated for login authentication.”
This meant the signup OTP endpoint could be weaponized to bypass the signin flow entirely.
Flaw 2: Signup OTPs could be generated for existing accounts
Even more concerning: the signup OTP endpoint didn’t check whether an account had already completed registration. We could send POST /otp?signup with a user_id for an account that was fully registered and active, and the system would happily generate an OTP and send it to that user’s email.
The endpoint only verified:
- Does this
user_idexist in the database? ✓
It didn’t verify:
- Has this account already completed signup?
- Is this a new registration in progress?
- Should this account be using the signin flow instead?
This is where the real danger lies, and why this vulnerability is so critical.
Problem 2: Signin Didn’t Track Password Verification
The signin process had a critical flaw in its session management – or rather, lack thereof.
When a user successfully entered their email and password, the system verified the credentials and then redirected them to the OTP verification page. But here’s what it didn’t do: it didn’t create any server-side session tracking. It didn’t store any state indicating “user with ID a3d5f891-4c2e-4b8a-9d3f-8e7c1b2a4f6d just successfully verified their password 30 seconds ago.”
So when the final OTP verification happened on the POST /verify-code endpoint, the system only validated two things:
- Was this confirmation_token recently issued? (Did we send an OTP?)
- Was the provided OTP code correct?
It never checked: “Did this person actually enter the correct password before getting here?”
The sign in OTP verification endpoint had no memory of what happened in previous steps.
Connecting the Dots – The Full Attack:
When we combined these two flaws, we had a complete account takeover method that completely bypassed password authentication.
Here’s the critical insight that makes this vulnerability devastating:
UUIDs Don’t Protect You From Data Breaches or Social Engineering
The platform did everything “right” from a textbook security perspective – used cryptographically secure UUIDs, encrypted data, hashed passwords, and implemented two-factor authentication. But here’s the harsh reality: user IDs get exposed all the time, through channels completely outside your control.
User IDs leak through previous data breaches (API logs, analytics platforms, email marketing tools, payment processors), social engineering attacks (phishing employees, compromising support agents), third-party breaches (OAuth providers, cloud storage misconfigurations, backup systems), and insider threats. When companies implement UUIDs and encryption, they often think they’re safe – but this vulnerability proves that assumption catastrophically wrong.
Even with all those security measures in place, an attacker who obtains user IDs (through any of these methods) can take over accounts without ever needing the password.
The Attack Flow:
Step 1: Attacker obtains user IDs from a third-party breach, dark web marketplace, or compromised service that had access to the platform’s data. For example: user_id: a3d5f891-4c2e-4b8a-9d3f-8e7c1b2a4f6d
Step 2: They send a direct API request:
POST /otp?action=signup user_id=a3d5f891-4c2e-4b8a-9d3f-8e7c1b2a4f6d //user-account already exists on the platform
Step 3: The system generates a legitimate OTP and sends it to that user’s email address. It returns a confirmation_token in the response.
Step 4: With the valid confirmation_token, attacker sends automated requests to POST /verify-code systematically trying all possible 6-digit OTP combinations. We achieved 4,000 requests per second – complete enumeration in under 5 minutes.
Step 5: Once the correct OTP is found through brute-force, attacker will submit it with the confirmation_token and gain full account access.
No password needed. No encryption cracked. Just a logical flaw in the routine sign up business operations.
Real-World Impact:
This vulnerability fundamentally breaks the security model most platforms rely on. An attacker with exposed user IDs can drain crypto wallets, execute unauthorized trades, change account credentials to lock out legitimate owners, and do it all at scale – potentially compromising thousands of accounts simultaneously. Users lose funds with no warning, often unable to prove they didn’t authorize access since legitimate OTPs were used. For the platform, this creates a catastrophic scenario: every third-party integration becomes an attack vector, historic breaches can be weaponized against current users, there’s no clear breach attribution (passwords weren’t compromised), and the reputation damage is terminal. The most dangerous aspect is that all your security measures – encryption, password hashing, UUIDs, 2FA – become irrelevant theater. It’s like building a fortress with armed guards but leaving a side door unlocked. The logic connecting security components was broken, and that’s all it takes.
Recommended Fix:
The core issue is that each authentication endpoint operates independently without verifying the preceding steps. Implement server-side session management that cryptographically binds OTP verification to password authentication, the system must confirm the password was verified in the same session within 5 minutes before accepting an OTP. Completely separate signup and signin token systems so tokens from one flow fail in the other. Make the /otp endpoint require cryptographic proof of the preceding step (form submission or password entry), not just a valid user_id. Add strict rate limiting: 3 attempts per token, 5 per user_id per hour, with account lockout after failures regardless of IP address. Use time-limited (5 minutes), single-use tokens bound to the session and device. Monitor for suspicious patterns like direct API calls bypassing normal flows. The principle: possessing a user_id grants nothing, every operation must validate the complete authentication chain with cryptographic proof.
Vulnerability #2: IDOR in Redeem Functionality – Locking Users Out
Application Type:
A Web3 DeFi platform enabling users to convert fiat into crypto assets.
Vulnerability Description & Severity:
High Severity – Insecure Direct Object Reference (IDOR) in the redeem request API allowed any authenticated user to create redemption requests on behalf of other users, causing account lockouts and denial of service. Didn’t understand? Let’s deep dive into this.
Behind the Scenes (Discovery):
IDOR vulnerabilities are deceptively simple but devastatingly effective. They occur when an application trusts user-supplied input to determine which resources to access – without verifying that the user actually owns those resources.
We were testing the platform’s redemption feature – a critical function that allows users to convert their crypto holdings back to withdrawable assets. When a user initiates a redemption, the platform creates a blockchain transaction, and the account enters a “pending” state until the transaction is verified. During this time, the user cannot initiate new transactions – a necessary safeguard to prevent double-spending.
While testing this flow, we intercepted the API request and noticed something interesting: the request included a userId parameter. We asked ourselves the obvious question: “What happens if we change this to someone else’s ID?”
Technical Flow:
The redemption feature worked like this: A user navigates to the redeem page, enters the amount they want to redeem, and submits the form. Behind the scenes, the application generates a blockchain transaction with the redemption details and sends it to the backend API for processing.
The API request looked like this:
POST /redeem-request HTTP/2 Host: platform.example.com Content-Type: application/json Authorization: Bearer [user's_auth_token] { "txHex": "84ad00d901...363365", "txHash": "d5106a...81bee", "userId": "713awdh1-33....bb-2bbe", }
The txHex contains the encoded blockchain transaction, txHash is its identifier, userId specifies whose account this redemption belongs to, and provider indicates which blockchain network is being used.
When the backend received this request, it performed several validations:
- Is the auth token valid? ✓
- Is the transaction hex properly formatted? ✓
- Is the transaction hash valid? ✓
- Does the userId exist in the database? ✓
But here’s what it didn’t check: Does the authenticated user (the one making the request) actually own this userId?
The Vulnerability:
We modified the userId parameter in the request to point to a different user’s account – keeping everything else the same, including our own authentication token. We sent the request.
The system accepted it. It created a redemption request for the victim’s account, initiated by us, but attributed to them.
Here’s what happened on the victim’s side: Their account immediately entered a “pending redemption” state. A transaction appeared in their history that they never initiated. Their account was locked – they couldn’t make trades, couldn’t initiate new redemptions, couldn’t perform any financial operations. The system thought they had a pending transaction that needed to be resolved first.
The attack was trivial. All we needed was:
- Our own authenticated account (free to create)
- The victim’s
userId(exposed through various channels we discussed earlier – data breaches, API responses, social engineering) - The ability to craft a single API request
We didn’t need elevated privileges. We didn’t need to compromise their account.
Real-World Impact:
This vulnerability enables targeted denial-of-service attacks against any user on the platform. An attacker can systematically lock accounts of high-value users, competitors, or specific targets, preventing them from trading during critical market movements or time-sensitive opportunities. For users, this means potential financial losses from missed trading opportunities, inability to withdraw funds during market volatility, and forced account lockouts with no clear resolution path. For the platform, mass exploitation could lock hundreds of accounts simultaneously, overwhelm support systems with complaints, damage reputation as users publicize being unable to access their funds, and create regulatory scrutiny around platform stability and security controls. The attack requires minimal sophistication – just a valid account and basic API knowledge – making it easily scalable.
Recommended Fix:
Implement proper authorization checks on the /redeem-request endpoint: extract the authenticated user’s ID from their session/JWT token and validate it matches the userId in the request body before processing – reject any request where these don’t align. Never trust client-supplied user identifiers for security-critical operations; always derive the user context from the authenticated session. Additionally, implement rate limiting on redemption requests (max 5 per user per hour) to prevent abuse even if authorization is bypassed, and add server-side logging that flags mismatched userId attempts for security monitoring. The principle: user identity for authorization must come from the authentication token, never from request parameters.
Vulnerability #3: Account Takeover via Brute-Force
Application Type:
A Web3 asset conversion platform that allows users to convert between different cryptocurrencies and digital assets.
Vulnerability Description & Severity:
Critical Severity – The platform’s authentication system had no rate limiting and leaked information about user accounts through error messages, allowing attackers to brute-force both passwords and OTPs at scale.
Behind the Scenes (Discovery)
During our assessment, we started testing the login flow like any user would – entering an email and password, receiving an OTP, logging in. Simple enough. But then we asked ourselves: what happens if we get it wrong? What happens if we get it wrong a thousand times? Ten thousand times?
The answer? Nothing. The application just kept accepting our attempts, no questions asked.
Imagine you’re trying to guess someone’s 4-digit PIN for their phone. If you could only try once per minute, it would take you days or weeks to crack it. But what if there was no waiting time? What if you could try 10,000 combinations every single second? Suddenly, that “secure” PIN becomes worthless in minutes.
That’s exactly what we found here. The platform had no “speed limit” on login attempts – a security control called rate limiting. Rate limiting is like a bouncer at a club who says “slow down” when someone’s trying too hard to get in. It’s a basic but crucial defense that stops attackers from making millions of automated guesses.
Technical Flow: Here’s how the login process worked:
Step 1: User submits credentials
Request: POST /signin Parameters: email, password Response: If valid, returns an access_token
Step 2: User submits OTP
Request: POST /otp Parameters: access_token (from step 1), otp Response: Grants access if OTP is correct
The problems we found:
Problem 1: The Application Was Too Helpful
When we typed in a random email address that didn’t exist, the system told us: “This email address does not exist.” When we tried a real user’s email with a wrong password, we got a different message.
Why does this matter? It’s like knocking on doors in a neighborhood. If someone yells “nobody lives here!” you know to move on. If someone yells “wrong key!” you know someone’s home – you just need to find the right key. Within minutes, we could identify every single registered user on the platform.
Problem 2: No Speed Limits
We could send 5,000 login attempts per second. To put that in perspective: a 6-digit OTP has only 1 million possible combinations (000000 to 999999). At 5,000 attempts per second, we could try every single combination in less than 7 minutes.
Passwords? Even easier. Most people reuse passwords from other breached websites, or use common patterns like “Password123!” or “CompanyName2024”. With no rate limiting, an attacker can test millions of common passwords in minutes.
Real-World Impact:
Here’s what an attacker could actually do:
- Step 1: Scrape a list of all registered users (thanks to those helpful error messages)
- Step 2: Run automated scripts that try common passwords for each user account – pulling from databases of billions of leaked passwords from past breaches
- Step 3: Once they get the password right, brute-force the OTP in under 2 minutes
- Step 4: Log in as that user and take complete control of their account
Once inside, they could:
- Steal all crypto assets from the user’s wallet
- Execute unauthorized trades and conversions
- Transfer funds to attacker-controlled addresses
- Drain the account completely
The scariest part? This attack requires no advanced hacking skills. Just a basic script, some computing power, and patience.
Recommended Fix:
Implement account lockout after 3 failed login or OTP attempts with magic link recovery via email to unlock. Add strict rate limiting on all authentication endpoints (/signin, /otp) tied to the account being targeted, not IP addresses – attackers easily bypass IP-based controls using VPNs or distributed infrastructure. Lock accounts after 3 failures regardless of whether attempts come from one IP or hundreds. Use generic error messages (“Invalid credentials”) for both non-existent emails and wrong passwords to prevent username enumeration. The principle: rate limits must be account-based, and client-facing responses must never leak information about account existence or validity.
Closing:
Here’s the uncomfortable truth: every vulnerability we’ve shared was in a production platform. Real users. Real money. Real consequences waiting to happen.
The founders thought they were secure. They had experienced developers, multiple code reviews, and passing audits. But they missed what matters most—understanding how an attacker actually thinks. Not following checklists. Not running automated scans. Actually breaking down each component and asking: “Where does the logic fail?”
Your smart contract audit gave you a clean report. Great. But can someone brute-force every account in 10 minutes? Can they create redemption requests that lock users out of their own funds? Are there logical gaps in your authentication flow that make passwords optional?
You don’t know what you don’t know. And that’s exactly what attackers exploit.
Over the past year, we’ve identified over 100 vulnerabilities across Web3 platforms—critical flaws, logical gaps, and architectural failures that could have destroyed businesses overnight. What we’ve shared in this series is just a fraction of what we encounter. It’s impossible to document every finding, every near-miss, every disaster we’ve prevented. But we share what we can because the patterns repeat, and the stakes are too high to stay silent.
And we’re just getting started. In Part 2 of this series, we’re covering even more critical issues—KYC systems bypassed with a browser proxy, private keys exposed through APIs, and secrets hiding in plain sight. If you thought authentication bypasses were serious, wait until you see what else we found.
At Securze, we’ve stopped attacks before they happened. We’ve found the critical flaws that would have made headlines—the ones that end platforms, destroy reputations, and cost millions. Not because we’re lucky. Because we don’t think like auditors. We think like attackers.
The question isn’t whether vulnerabilities exist in your platform. They do. The question is: will you find them first, or will someone else?
Every day you wait is another day you’re exposed.
Get your application tested by Securze. Let us find what’s hiding before someone else does.
Links: Read Web3 Security Threats – Part 2 | Schedule Your Assessment


