Security flaws you can't ignore

How to avoid leaving your code's backdoor wide open

Walter Gandarella • February 27, 2025

Do you know what the biggest security problem with your application or SaaS is? No, it’s not that sophisticated exploit you saw at the latest security conference, nor some super complex network attack. The biggest threat is literally "between the chair and the monitor." Yes, I’m talking about you, developer, who’s leaving the backdoor wide open without even realizing it.

The problem isn’t that your code might be hacked. The problem is that, in many cases, you’ve already left it practically open. And the worst part? Many of these vulnerabilities are so basic that leaving them there after reading this article would be almost like self-sabotage.

I won’t focus on major CVEs or network vulnerabilities. After all, you’re probably hosting on Vercel, Google, Amazon, or another company with decent layers of protection. You’re likely using an ORM that makes SQL injection difficult and building interfaces with JSX that reduce XSS risks. This doesn’t mean these attacks are impossible, but there are hundreds of articles about these vulnerabilities out there. Let’s focus on what’s really leaving your application vulnerable on a daily basis.

Webhooks: The Unprotected Backdoor

Everyone loves webhooks, right? They’re a practical way to integrate systems. But have you noticed that almost everyone uses the same default route? /api/webhook or /api/hook. This alone isn’t a problem, but it becomes one when anyone with malicious intent can easily guess it and, worse yet, send you fake data.

Imagine a user sending a request to your webhook route pretending to be your payment gateway, confirming a purchase that never happened. Scary, right?

The solution is simple: whenever you set up a webhook, use a secret signature. In Stripe, you receive the stripe-signature header, while in Mercado Pago (from Brazil), there’s the x-signature. Your backend should validate this code to ensure the request came from the correct source. When a malicious user tries to bypass the system, your API will simply complain about the missing signature.

IDOR: When Your Application Hands Over Other Users’ Data

IDOR (Insecure Direct Object Reference) is that flaw where permissions aren’t checked when accessing objects via an API. It’s more common than you think and sends shivers down your spine just thinking about the consequences.

Imagine you have an endpoint /api/purchase/:id where users access their purchase details. The code is simple: the user makes a GET request to /api/purchase/123, and you return the details. But what if the purchase with ID 123 isn’t theirs? Congratulations, you’ve just exposed another customer’s data!

Another classic mistake is having a PATCH /api/profile endpoint where the server receives the user ID in the request body. Never do that! The user ID should always come from the session, JWT, or another authentication mechanism, never from the request body, which can be easily manipulated.

The rule is clear: always validate who’s requesting access before showing, editing, or deleting anything.

Data Exposure: Less Is More

This problem is subtler but equally devastating. Let’s say you created a marketplace where you can access product details through a GET request with the product ID. The API returns the product details and some seller information. So far, so good.

But what if, along with the seller’s name and photo, you also end up sending their email, tax ID, phone number, address, and even their encrypted password? This happens when you query the product, fetch all the related seller data, and forget to filter what should or shouldn’t be sent to the frontend.

The solution is: send only what’s really necessary. If the frontend only needs the seller’s name and photo, send only that. Don’t fall into the trap of thinking, "Oh, the frontend will only use what it needs." Protect your users by limiting the data exposed from the start.

Rate Limit: Protecting Yourself from Quantity

Not implementing request limits may not seem like a direct vulnerability, but it’s certainly one of the things that harms a product the most. Imagine your application has a public API for creating posts. Without limits, an attacker can automate this request and create thousands of fake posts in seconds.

This not only pollutes your database and degrades the user experience but can also cost you money—literally. Database storage isn’t free, and without protection, you’ll end up paying for junk data.

Another example: an API for sending emails. An attacker could easily blow through your sending limit, forcing you to pay additional fees to continue operating normally.

On login pages, the absence of rate limits allows brute force attacks to discover passwords. Implement CAPTCHAs, attempt limiters, or other protection mechanisms on sensitive endpoints or those that involve costs.

Mass Assignment: When You Become an Admin by Accident

This vulnerability is one of my favorites because it’s so simple and so dangerous at the same time. When you have a PATCH route to modify some information about an object, like a username or product description, this flaw allows modifying any property of the object.

Besides changing your username, you could change your role to admin or any other system property. The solution? Explicitly define which fields can be modified. Don’t leave this open.

TOCTTOU: The Most Elegant Vulnerability

Time of Check to Time of Use (TOCTTOU) is a fascinating vulnerability that occurs when there’s a gap between checking a condition and using the result of that check.

The classic example is a banking operation. You have €100 in your account and request a withdrawal of that amount. The program checks if you have the money and, if so, processes the withdrawal. But what if you send two requests simultaneously?

In practice, it’s hard to send exactly two requests at the same time, so usually, multiple requests are sent. Due to network delay, multiple instances of the function will run in parallel, all passing the balance check and then processing the withdrawal multiple times.

This works for many scenarios: likes on posts, purchasing limited tickets, and various other operations that depend on finite resources.

The solution is relatively simple: you need to "lock" critical resources while they’re being used. You can use semaphores, queue systems, but the most common are database transactions, where the check and action happen atomically—either the operation completes entirely, or nothing happens, with no interruptions.

Trusting the Frontend: The Fundamental Mistake

This is perhaps the most basic and also the most common mistake: believing that frontend validations are enough to ensure your application’s security.

Many people think, "If the button is disabled, the request will never be sent" or "If the frontend shows the user has €10, they can only withdraw €10, after all, it was my server that provided that value." Not quite.

Any rule implemented in the frontend can be ignored or manipulated by the user. This is especially true in applications that use Client-Side Rendering (CSR).

To illustrate: imagine a finance app where the withdrawal button is disabled when you have no balance. A malicious user can easily inspect the code, find the conditional rendering that controls this display, alter the relevant variables at runtime, and enable the button.

Of course, if the backend is well implemented, the withdrawal request will fail. But that’s not always the case. Think of an e-commerce site that calculates prices on the frontend and sends that value to the backend. If someone intercepts the request and changes the price from €500 to €50, and your backend blindly trusts that information, you’ll have a serious problem.

The rule is clear: always recalculate prices and verify all conditions on the server. Never, ever trust the frontend for security. If your system relies solely on the frontend for protection, someone will bypass it.

Is Your App 100% Secure?

No. And it never will be.

100% security doesn’t exist and never will. Think about it: every program you use to create or host your application was made by humans, and humans make mistakes. It could be a bug in your code, the framework you use, the database, the operating system… Everything was built by people who, at some point, may have made a single error capable of compromising the entire system.

Even if your code were perfect (which is practically impossible), nothing would stop someone from physically invading your server, or an employee at your hosting provider stealing your credentials, or you falling for a sophisticated phishing attack.

Security is an infinite game. The goal isn’t to be 100% secure but to make attacks as difficult as possible and minimize damage if something goes wrong. It’s a continuous process of improvement, vigilance, and adaptation.

Speaking of which, at Yes Marketing, we’re already a step ahead in this game. We’ve implemented measures to mitigate all these vulnerabilities in our systems. Our team not only fixed the most common gaps but also set up a continuous monitoring process to identify potential new flaws. We sleep better knowing we’re not just reacting to problems but actively seeking ways to improve our security before any incident occurs. As we say around here: better safe than having to explain to the boss why the system was hacked, right?

You might have recognized some of these flaws in your own projects. Don’t despair—identifying the problem is the first step to solving it. Security doesn’t have to be complicated, but it has to be taken seriously.

And remember: if after reading all this you’re still making these mistakes, it’s no longer carelessness. It’s self-sabotage.


Latest related articles