Vulnerabilities Every Developer Should Know — [Part 1]
A developer’s job does not end when an API returns 200 OK, a database query succeeds, or a payment gateway redirects the user to a success page.
There is another side to software development that every developer must understand: How can someone misuse the functionality that we have built?
Most applications trust their users to follow the intended workflow. Users are expected to access their own data, upload legitimate files, follow the checkout process, and pay the correct amount.
But attackers do not follow the workflow presented by the frontend.
They can inspect HTTP requests, modify parameters, send requests directly to APIs, change resource identifiers, manipulate application state, and exploit assumptions made by backend developers.
This article covers four vulnerabilities that every backend and full-stack developer should understand:
- Insecure Direct Object Reference (IDOR).
- Directory Traversal (Path Traversal).
- Insecure File Upload.
- Business Logic Vulnerabilities in Payment Systems.
We will look at these vulnerabilities from a backend engineering perspective. We will understand what happens inside the application, why the vulnerability exists, what damage it can cause, and how developers can prevent it.
1. Insecure Direct Object Reference (IDOR)
What is IDOR?
IDOR stands for Insecure Direct Object Reference.
It is an access control vulnerability that occurs when an application allows a user to access or modify an object without checking whether that user is authorized to interact with it.
Let’s understand this with a simple example.
Imagine that you are building a banking application.
Every user can view their transaction history through an API.
Your backend exposes the following endpoint:
GET /api/transactions/1001
The number 1001 represents a transaction ID in your database.
A logged-in user sends this request and receives their transaction details.
Everything looks fine.
But what happens if the user changes the transaction ID?
GET /api/transactions/1002
If the backend returns another user’s transaction details, you have an IDOR vulnerability.
The problem is not that the attacker guessed the ID.
The problem is that your backend never checked whether the attacker had permission to access that transaction.
Authentication and authorization are two different things.
- Authentication answers: Who are you?
- Authorization answers: Are you allowed to perform this action?
A user can be properly authenticated and still be unauthorized to access a particular resource.
This is the fundamental mistake behind IDOR.
Understanding the backend concept
Let’s understand how this vulnerability develops inside a typical backend application.
Suppose we have two users in users table:
and two documents in documents table :
Documents table
Alice logs into the application.
The authentication middleware verifies her session or JWT and identifies her as user 101.
Now, she requests:
GET /api/documents/501
The backend retrieves the document using its ID.
A vulnerable implementation might look like this:
app.get("/documents/:id", async (req,res) => {
const document = await db.document.findUnique({
where: {
id: Number(req.params.id)
}
});
return document;
});Do you see the problem?
The backend uses the document ID to retrieve the record, but it never checks who owns that document.
The authentication middleware might be working perfectly. The database might also be working perfectly.
The authorization logic is missing.
Alice can potentially access Bob’s document by changing the requested ID.
How should the backend work?
The backend must verify ownership before returning the resource.
A safer implementation would look like this:
app.get("/documents/:id", async (req, res) => {
const document = await db.document.findFirst({
where: {
id: Number(req.params.id),
userId: req.user.id
}
});
if (!document) {
return req.status(404).json({message:"Document not found"})
}
return document;
})Notice the difference.
The database query now checks two conditions:
- Does the document exist?
- Does it belong to the authenticated user?
This is called object-level authorization.
The important part is that the user ID comes from the authenticated server-side identity, not from a user-controlled request parameter.
In a multi-tenant application, the same principle applies to organizations, teams, projects, invoices, and other resources.
For example, a user should only access a project if their account belongs to the organization that owns that project and their role allows the requested operation.
Where can IDOR occur?
IDOR is not limited to changing IDs in URLs.
It can occur anywhere an application accepts a reference to a resource.
1. URL parameters
GET /api/invoices/5001Changing the invoice ID might expose another customer’s invoice.
2. Request body
{
"documentId": 501,
"action": "delete"
}The backend must verify ownership before deleting the document.
3. Query parameters
GET /api/orders?userId=101If the backend trusts the supplied user ID instead of the authenticated identity, users might access other accounts’ orders.
4. File references
Imagine an application that stores uploaded documents using predictable identifiers.
/uploads/1001.pdf
/uploads/1002.pdf
/uploads/1003.pdfIf the application does not enforce access control when serving these files, users might access documents belonging to other users.
5. API endpoints that modify data
IDOR is not only about reading private information.
Consider:
PATCH /api/users/102/profileIf Alice can modify Bob’s profile, the vulnerability has become an unauthorized data modification issue.
The same problem can affect:
- Deleting another user’s account.
- Modifying order details.
- Changing account settings.
- Accessing private messages.
- Modifying support tickets.
- Viewing confidential invoices.
What damage can IDOR cause?
The impact depends on the resource being accessed.
Data disclosure: Attackers can access personal information, financial records, private documents, and business data.
Data manipulation: Attackers can modify another user’s records, addresses, orders, or account settings.
Data destruction: Unauthorized deletion of documents, records, or accounts can cause permanent data loss.
Account takeover: In some situations, access to sensitive account-management objects can lead to further compromise.
Business impact: A multi-tenant SaaS application with IDOR may expose one company’s data to another company’s customers.
OWASP classifies this issue under Broken Object Level Authorization in its API Security Top 10 (API1:2023). https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/
How to prevent IDOR
1. Never trust resource IDs supplied by the client
A client can send any ID.
Sequential IDs, UUIDs, slugs, and random identifiers are all user-controlled references when they arrive in a request.
Do not treat an identifier as proof of permission.
2. Enforce authorization at the backend
Every endpoint that accesses a protected resource must verify the user’s permission.
Do not rely on frontend checks.
Hiding a button does not prevent someone from calling the API directly.
3. Scope database queries to the authenticated user
Prefer queries that combine the resource ID with the current user’s ownership or access relationship.
For example:
SELECT *
FROM documents
WHERE id = $1
AND user_id = $2;Here, $2 must come from the authenticated server-side identity.
4. Use centralized authorization policies
If your application has many endpoints, do not implement authorization inconsistently.
Create reusable authorization policies or service-layer checks.
For example:
- Can this user read this document?
- Can this user modify this project?
- Can this user delete this invoice?
- Does this user belong to this organization?
5. Use UUIDs as an additional defense
UUIDs make resource identifiers harder to guess.
However, UUIDs do not fix IDOR.
If a user obtains another user’s UUID through logs, shared links, or another API, the application must still enforce authorization.
6. Test with multiple accounts
Create two test accounts.
- Account A creates a document.
- Account B attempts to access it.
- Account B attempts to modify it.
- Account B attempts to delete it.
Every unauthorized operation should fail.
This is one of the simplest and most useful authorization tests a backend developer can perform.
Remember: A valid login does not mean unrestricted access to every database record.
2. Directory Traversal (Path Traversal)
What is Directory Traversal?
Directory Traversal is a vulnerability that allows an attacker to access files or directories outside the location that an application intended to expose.
It occurs when an application uses user-controlled input to construct filesystem paths without properly restricting where those paths can point.
To understand this vulnerability, we first need to understand how applications interact with the filesystem.
Understanding the backend concept
Imagine that you are building a document management application.
Users can download their uploaded files.
Your application stores documents inside:
/app
/uploads
report.pdf
invoice.pdf
profile.png
server.ts
.envYou create an endpoint:
GET /download?file=report.pdfYour backend receives the filename and constructs a filesystem path.
Conceptually:
const filePath = "/app/uploads/" + filename;The application then reads the file and returns it to the user.
For a normal filename, this works correctly.
But filesystems support relative path navigation.
For example:
.refers to the current directory...refers to the parent directory./separates directory components on Unix-like systems.
This means that a path is not simply a filename. It can describe navigation through the filesystem.
If an application blindly accepts user-controlled paths, an attacker may be able to escape the intended upload directory.
This is the basic idea behind Path Traversal.
Why does ../ matter?
Let’s understand the filesystem structure.
Suppose the current directory is:
/app/uploads/A relative path containing .. can move to its parent:
/app/uploads/../The resulting location is:
/app/Another parent-directory reference can move further up.
This behavior is normal filesystem functionality.
The vulnerability occurs when a developer allows an untrusted user to control that navigation.
For example, a download endpoint that accepts arbitrary path input might accidentally expose files outside the intended directory.
The important point is that the operating system is not necessarily doing anything wrong. It is following the path it received.
The application failed to restrict the path.
How does the vulnerability happen?
Consider this vulnerable implementation:
app.get("/download", async (req,res) => {
const filename = req.query.file;
const filePath = `/app/uploads/${filename}`;
return Bun.file(filePath);
});The developer assumes that query.file will always contain a filename such as:
report.pdfBut HTTP requests are controlled by the client.
The server cannot assume that a user will only provide values generated by the frontend.
If arbitrary path input is accepted, the application might read files outside the upload directory.
The underlying mistake is using a user-controlled string as a filesystem path without validating the final resolved location.
What can an attacker access?
The impact depends on the operating system, application permissions, filesystem configuration, and the files available to the server process.
Potentially exposed information includes:
Environment configuration: Files containing database credentials, API keys, and other secrets.
Application source code: Source files can reveal internal logic, API routes, authentication mechanisms, and implementation details.
Configuration files: Database connection strings, cloud configuration, and deployment information.
System files: Depending on permissions, sensitive operating-system information may also be exposed.
The consequences can become more serious when exposed secrets are reused elsewhere.
For example, leaked database credentials may allow an attacker to access a database directly.
A file disclosure vulnerability can therefore become the starting point for a larger compromise.
Directory Traversal is not limited to reading files
There are two important categories.
1. Read-based Path Traversal
The application reads a file from an unintended location.
Possible impact:
- Disclosure of secrets.
- Exposure of source code.
- Leakage of internal configuration.
- Access to private application data.
2. Write-based Path Traversal
The application writes a file to an unintended location.
Imagine a file upload feature that constructs its destination using the original filename.
If the application does not restrict the resolved destination, an attacker might cause files to be written outside the intended upload directory.
Depending on the permissions and application architecture, this could overwrite important files or affect application behavior.
This is especially dangerous when the application process has excessive filesystem permissions.
How to prevent Directory Traversal
1. Do not accept arbitrary filesystem paths
The safest design is to accept a resource identifier rather than a filesystem path.
For example:
GET /api/files/8f21a4The backend looks up the file record in the database and obtains its internally stored storage location.
The user never chooses the physical filesystem path.
2. Canonicalize the path
Canonicalization resolves path components such as . and .. into a normalized absolute path.
For example, in Node.js:
import path from "node:path";
const uploadDirectory = path.resolve("/app/uploads");
const requestedPath = path.resolve(
uploadDirectory,
filename
);But normalization alone is not enough.
You must verify that the final path remains inside the intended directory.
A basic containment check is:
const relativePath = path.relative(
uploadDirectory,
requestedPath
);
if (
relativePath.startsWith("..") ||
path.isAbsolute(relativePath)
) {
throw new Error("Invalid file path");
}This is an illustrative containment check. Production implementations must also account for symbolic links, filesystem races, platform-specific behavior, and the way files are opened.
3. Use allowlists
If users are supposed to download only PDFs, allow only registered PDF resources.
Do not try to maintain a blacklist of dangerous path characters.
An allowlist is easier to reason about.
4. Apply least-privilege filesystem permissions
Your application process should not have unnecessary access to sensitive files.
For example:
- Upload services should not need access to application secrets.
- Public file-serving processes should have read-only access to approved files.
- Application containers should not run with unnecessary root privileges.
Even if a path traversal bug exists, restricted filesystem permissions can reduce its impact.
5. Store files separately
Where possible, store uploaded files in dedicated object storage or a separate storage service.
This reduces the connection between public file handling and the application’s sensitive filesystem.
6. Avoid leaking filesystem details
Error responses should not reveal absolute filesystem paths, internal directory structures, or sensitive configuration information.
Log detailed errors securely on the server.
Remember: Never assume that a filename supplied by the client is safe to use as a filesystem path.
3. Insecure File Upload Vulnerabilities
What is an Insecure File Upload vulnerability?
File upload functionality is present in almost every modern application.
Users upload:
- Profile pictures.
- PDFs.
- Documents.
- Videos.
- CSV files.
- Resumes.
- Attachments.
- Product images.
Developers often treat file uploads as a simple feature.
The frontend provides a file picker, the backend receives a multipart request, and the application saves the file.
But accepting files from untrusted users creates a large attack surface.
An Insecure File Upload vulnerability occurs when an application does not properly validate, restrict, process, or store uploaded files.
The problem is not that file uploads are inherently dangerous.
The problem is trusting files simply because the user selected an allowed file type in the frontend.
Understanding the backend concept
Let’s understand what happens when a user uploads a file.
Suppose you have a profile picture upload endpoint:
POST /api/profile/avatar
Content-Type: multipart/form-dataThe browser sends the file using multipart form data.
The backend receives several pieces of information:
- Original filename.
- File content.
- Content-Type.
- File size.
- Other multipart fields.
A simplified upload pipeline looks like this:
User
|
v
Frontend File Picker
|
v
HTTP Multipart Request
|
v
Backend Upload Endpoint
|
v
Validation
|
v
Storage
|
v
Database Metadata
|
v
File RetrievalEvery step requires security controls.
A common mistake is assuming that the file’s extension or MIME type proves what the file actually contains.
It does not.
How attackers exploit weak file validation
1. Extension-based validation
Imagine that your backend accepts files ending in .jpg.
A developer might write:
if (!file.name.endsWith(".jpg")) {
throw new Error("Invalid file type");
}This is not sufficient validation.
The filename is controlled by the client.
An extension does not prove the actual content of a file.
The application must validate the file using multiple independent checks.
2. MIME type spoofing
When a browser uploads a file, it can include a Content-Type header.
For example:
Content-Type: image/jpegDevelopers sometimes trust this value.
But the client can supply or manipulate request metadata.
An attacker can label a file as an image even when its content is something else.
Therefore, checking the MIME type header alone is not a reliable security control.
3. Malicious file content
A file can contain data that causes harm when another component processes or serves it.
Examples include:
- Files designed to exploit vulnerabilities in image-processing libraries.
- Malicious documents targeting vulnerable document parsers.
- HTML or SVG content that executes active browser-side content in an unsafe context.
- Archives designed to consume excessive CPU or memory.
- Files containing content intended to trick or phish users.
The danger is not always limited to the upload endpoint.
A file may be stored successfully and only become dangerous when another service processes it or when another user opens it.
4. Executable file uploads
Consider an application that stores uploaded files inside a directory served by a web server.
If the server is configured to execute certain uploaded file types, an attacker may be able to upload executable server-side content.
In a vulnerable configuration, this can potentially lead to Remote Code Execution (RCE).
However, an uploaded executable file does not automatically execute.
Execution depends on the web server, runtime configuration, storage location, and file handling behavior.
This distinction matters because simply finding an upload endpoint does not prove that RCE is possible.
5. Denial of Service through file uploads
Imagine an application that allows users to upload files without size limits.
An attacker could submit extremely large files or specially constructed archives.
Get Anuj parasar’s stories in your inbox
Join Medium for free to get updates from this writer.
The backend may consume:
- Disk space.
- Memory.
- CPU.
- Network bandwidth.
- Processing capacity.
This can make the application unavailable to legitimate users.
OWASP also highlights risks from ZIP bombs and other resource-intensive files.
What damage can insecure uploads cause?
Depending on the implementation, the impact can include:
Remote Code Execution: If uploaded content is executed by a vulnerable or incorrectly configured server.
Stored XSS: If uploaded HTML or SVG content is served in a context where the browser executes attacker-controlled scripts.
Data disclosure: If uploaded files are publicly accessible or their storage locations are predictable.
Denial of Service: If an attacker exhausts storage or processing resources.
File overwrite: If the application uses attacker-controlled filenames and allows collisions with existing files.
Malware distribution: If the application becomes a hosting platform for malicious files.
How to secure file uploads
File upload security requires multiple layers of validation.
No single check is sufficient.
1. Use an extension allowlist
Only accept file types required by your application.
For example, a profile picture feature might allow:
.jpg
.jpeg
.png
.webpDo not allow every file type and then attempt to block dangerous ones.
An allowlist reduces the attack surface.
2. Validate actual file content
Do not trust the filename or Content-Type header.
Use file-signature or magic-byte detection to identify the actual format.
For example, JPEG and PNG files have recognizable binary signatures.
However, signature detection alone is not a complete validation method. A file can have a valid signature and still contain malicious or malformed content.
3. Generate filenames on the server
Never use the original filename as the final storage filename.
Instead, generate a random identifier.
For example:
Original: my-profile-photo.jpgStored: 8f21a4c7-2d4e-4c8f.webpStore the original filename separately as metadata if your application needs it.
This helps prevent filename collisions, path manipulation, and accidental overwrites.
4. Enforce file size limits
Set limits at multiple levels:
- Reverse proxy.
- API server.
- Multipart parser.
- Application validation.
- Storage service.
Also consider the maximum number of files per request.
A file upload endpoint should not allow unlimited resource consumption.
5. Store files outside the webroot
This is one of the most important protections.
Do not store user uploads inside a directory where the web server might execute them.
Prefer:
- Dedicated object storage.
- A separate file storage service.
- A directory outside the application webroot.
When users request a file, the backend should authorize the request and serve the stored object through a controlled retrieval mechanism.
6. Disable execution in upload directories
If local filesystem storage is required, configure the web server so that uploaded files cannot execute as server-side scripts.
Use separate storage and serving permissions.
7. Scan and process files safely
Depending on the application, use:
- Antivirus scanning.
- Sandboxing.
- Content Disarm and Reconstruction (CDR) for supported document types.
- Safe image re-encoding.
- Updated file-processing libraries.
For example, if users upload profile pictures, you can decode and re-encode the image into an approved format before making it available.
8. Protect file retrieval
An uploaded file should not automatically become publicly accessible.
If the file is private, the download endpoint must enforce authorization.
A secure upload system also needs a secure download system.
9. Protect the upload endpoint itself
Require authentication where appropriate.
Apply rate limits and quotas.
Protect cookie-authenticated upload endpoints against CSRF.
Log upload failures and unusual activity.
OWASP’s File Upload Cheat Sheet provides a detailed set of controls for these scenarios:
https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html
Remember: File upload security does not end when the file has been saved. It continues when the file is processed, stored, accessed, and served to other users.
4. Payment Logic Vulnerabilities — How Can Someone Get Products or Services for Free?
This is one of the most interesting categories because it demonstrates that an application can have perfectly working authentication, authorization, and database queries while still losing money.
Imagine an e-commerce application.
A customer adds a product worth ₹10,000 to their cart.
They proceed to checkout, complete the payment process, and receive the product.
But what happens if the backend marks the order as paid without verifying that the payment actually succeeded?
The customer gets the product without paying.
This is an example of a Business Logic Vulnerability.
More specifically, payment-related issues can involve:
- Payment validation bypass.
- Price manipulation.
- Payment status manipulation.
- Payment workflow bypass.
- Coupon and discount abuse.
- Refund abuse.
- Race conditions.
- Webhook verification failures.
These are related but distinct weaknesses.
Let’s understand them from the backend perspective.
First, understand how a normal payment system works
Before discussing vulnerabilities, we need to understand a typical payment architecture.
Suppose you are building an e-commerce application using:
- Node.js or Elysia for the backend.
- PostgreSQL for the database.
- Redis for caching.
- A payment gateway such as Razorpay or Stripe.
A simplified payment flow looks like this:
Customer
|
v
Frontend Checkout
|
v
Backend Creates Order
|
v
Payment Gateway
|
v
Customer Completes Payment
|
v
Payment Gateway Confirms Result
|
v
Backend Verifies Payment
|
v
Order Marked as PAID
|
v
Product FulfillmentThe most important part is the final verification.
The frontend must never be the authority that decides whether a payment succeeded.
A redirect to a success page is not proof of payment.
A client-side message saying payment_success: true is not proof of payment.
A user can manipulate client-side state and send requests directly to your backend.
The backend must independently verify the payment using trusted payment-provider mechanisms.
Vulnerability 1: Payment Status Manipulation
Imagine a developer implements the following flow:
- The customer clicks Pay.
- The payment gateway processes the transaction.
- The frontend receives a success response.
- The frontend calls the backend.
- The backend changes the order status to
PAID.
A vulnerable endpoint might look conceptually like this:
app.post("/orders/:id/complete", async ({ params, body }) => {
if (body.paymentStatus === "success") {
await db.order.update({
where: {
id: params.id
},
data: {
status: "PAID"
}
});
}
return { success: true };
});What is wrong here?
The backend trusts a value supplied by the client.
The client is effectively deciding whether the transaction succeeded.
An attacker could potentially manipulate the request and cause the application to mark an unpaid order as paid.
The backend has confused a user-provided statement with a verified payment result.
How to prevent it
The backend should maintain the payment state.
A safer flow:
- Create an order on the backend.
- Calculate its amount on the backend.
- Create a payment session or payment order with the provider.
- Store the provider’s transaction reference.
- Receive the payment result through a trusted provider integration.
- Verify the result.
- Check that the payment amount, currency, and order reference match.
- Mark the order as paid only after successful verification.
The browser can display payment status, but it cannot decide payment status.
Vulnerability 2: Price Manipulation
This is another common payment-related mistake.
Imagine that a product costs ₹5,000.
Your frontend sends the following request:
{
"productId": "101",
"quantity": 1,
"price": 5000
}A vulnerable backend might calculate the order total using the supplied price.
The problem is that the client controls the request.
The price should not be trusted simply because it was originally generated by your frontend.
A secure backend should accept the product ID and quantity, retrieve the current product price from its own database, and calculate the total.
For example:
const product = await db.product.findUnique({
where: {
id: productId
}
});
if (!product) {
throw new Error("Product not found");
}
const total = product.price * quantity;The backend must also validate that the quantity is a positive integer and within the product’s purchase limits.
Why does this matter?
If the backend trusts client-supplied prices, attackers may be able to manipulate the amount charged.
The same problem can affect:
- Shipping charges.
- Taxes.
- Discounts.
- Donations.
- Subscription plans.
- Product quantities.
- Currency conversion.
OWASP’s Payment Functionality testing guide specifically discusses price tampering and quantity tampering as payment security risks.
Vulnerability 3: Payment Gateway Integration Mistakes
A payment gateway is a third-party service that processes financial transactions.
Your application communicates with it through APIs, redirects, and sometimes webhooks.
A common architectural mistake is treating a browser redirect as the final source of truth.
Consider this flow:
Customer
|
v
Payment Gateway
|
v
Redirect to /payment/success
|
v
Backend Marks Order as PaidThis is unsafe if the backend marks the order as paid simply because the customer reached the success URL.
A user can visit that URL without completing a payment.
The application must not confuse navigation with transaction confirmation.
What is a webhook?
A webhook is an HTTP request sent by one service to another when a particular event occurs.
For example, a payment provider may send your backend an event indicating that a payment has succeeded.
Your application might expose:
POST /webhooks/paymentThe provider sends a notification containing information about the transaction.
Your backend processes the event and updates the order.
But there is another security problem.
Can your backend prove that the webhook actually came from the payment provider?
If it accepts arbitrary requests at the webhook endpoint, an attacker might send forged payment notifications.
How to secure webhooks
- Verify the provider’s webhook signature using its official verification procedure.
- Use the exact raw request body when the provider’s signature scheme requires it.
- Check that the event belongs to the expected merchant account.
- Verify the payment reference and order reference.
- Verify the amount and currency.
- Process only supported event types.
- Handle duplicate events safely.
- Keep a record of processed provider event IDs.
Never assume that a request is genuine just because it reaches your webhook URL.
Also, do not assume that a webhook’s successful delivery means the business operation has been completed correctly.
The application must validate the event and its relationship to the order.
Vulnerability 4: Race Conditions in Payment Systems
Now let’s discuss something that backend developers should pay special attention to.
A race condition occurs when multiple operations access or modify shared state at the same time without proper synchronization.
This can create unexpected results.
Imagine that a user has a gift card worth ₹1,000.
The application allows the gift card to be redeemed once.
A simplified backend flow might look like this:
1. Check gift card balance.
2. Check whether gift card is already used.
3. Deduct the balance.
4. Mark gift card as used.What happens if two requests reach the backend almost simultaneously?
Both requests may check the gift card before either request updates it.
Both requests see:
Balance: ₹1000
Status: UNUSEDBoth requests continue.
If the backend does not perform the check and update atomically, the same gift card might be redeemed more than once.
This is a race condition.
Understanding TOCTOU
TOCTOU stands for Time of Check to Time of Use.
It describes a situation where a condition is checked at one point in time, but the system acts on that condition later.
Between those two operations, another request may change the underlying state.
For example:
Request A: Check balance = ₹1000
Request B: Check balance = ₹1000Request A: Deduct ₹1000
Request B: Deduct ₹1000The result depends on how the backend handles concurrency and database updates.
The same problem can affect:
- One-time coupons.
- Gift cards.
- Refund requests.
- Wallet balances.
- Inventory reservations.
- Referral rewards.
- Subscription renewals.
- Payment callbacks.
How to prevent race conditions
Use database transactions.
Related operations should be executed as one atomic transaction.
For example, PostgreSQL supports row-level locking.
A transaction can lock a gift card record before checking and updating its state.
Conceptually:
BEGIN;
SELECT *
FROM gift_cards
WHERE id = $1
FOR UPDATE;
-- Validate balance and redemption status.
UPDATE gift_cards
SET balance = 0,
status = 'REDEEMED'
WHERE id = $1;
COMMIT;UPDATE gift_cards
SET balance = 0,
status = 'REDEEMED'
WHERE id = $1;COMMIT;if (!product) {
throw new Error("Product not found");
}
const total = product.price * quantity;
The application must check the locked record’s state and reject invalid operations before updating it.
Other options include:
- Conditional atomic updates.
- Optimistic locking with version columns.
- Unique database constraints.
- Idempotency keys.
- Distributed coordination where appropriate.
A Redis lock alone should not replace database-level correctness for critical financial operations.
A real-world example: The Uber free-ride vulnerability
Let’s connect these concepts to an actual security incident.
In March 2017, TechCrunch reported on a vulnerability discovered by security researcher Anand Prakash.
The vulnerability allowed him to take Uber rides without being charged.
He had permission from Uber to test the issue in the United States and India.
According to the report, the problem occurred when specifying a payment method.
The researcher found that an invalid payment method value could be accepted in a way that allowed the ride to proceed without billing the account.
Uber fixed the vulnerability after it was reported through its bug bounty program and rewarded the researcher with $5,000.
Source: https://techcrunch.com/2017/03/04/researcher-finds-bug-that-allows-free-uber-rides/
What should developers learn from this?
The important lesson is not that payment gateways are inherently insecure.
The lesson is that payment-related business rules must be enforced by the backend.
A payment method must be validated.
A payment must have a verified result.
An order must not be fulfilled simply because a client request claims that the transaction succeeded.
The application must enforce the relationship between:
- The customer.
- The order.
- The selected payment method.
- The payment transaction.
- The amount.
- The final order status.
The Uber incident is a useful example of a payment validation and business logic flaw. It should not be described as a confirmed webhook-signature vulnerability or a generic price-tampering exploit. The published report identifies invalid payment-method handling as the issue.
Designing a secure payment workflow
Let’s put everything together.
A production-grade payment flow should have explicit states.
For example:
PENDING
|
v
PAYMENT_INITIATED
|
v
PAYMENT_PROCESSING
|
+------> FAILED
|
v
PAID
|
v
FULFILLEDThe backend should control which transitions are valid.
For example:
- An order cannot become
PAIDbecause the frontend sends that value. - An order cannot be fulfilled while payment is pending.
- A failed payment cannot trigger product delivery.
- A completed payment event cannot be processed repeatedly to create duplicate fulfillment.
- The payment amount must match the expected order amount.
The backend should verify that the payment belongs to the expected order and that the amount and currency match before transitioning the order to PAID.
Why is idempotency important?
Payment providers may retry webhook delivery.
Your backend may also retry operations after network failures.
This means the same event can arrive more than once.
If your application processes the same successful payment event repeatedly, it could accidentally:
- Deliver the same product multiple times.
- Add wallet credits repeatedly.
- Create duplicate invoices.
- Trigger duplicate subscriptions.
Idempotency means that processing the same logical operation repeatedly should not produce duplicate side effects.
Use provider event IDs, unique database constraints, and carefully designed transaction boundaries to ensure that duplicate events do not cause duplicate fulfillment.
For financial operations, idempotency is not an optional convenience. It is an important correctness property.
Payment security checklist
Before deploying a payment feature, ask:
- Are prices calculated on the backend?
- Are quantities validated?
- Are discounts and coupons validated on the backend?
- Are payment methods validated?
- Does the backend independently verify successful payments?
- Are webhook signatures verified?
- Are order IDs and payment references associated correctly?
- Are payment amounts and currencies checked?
- Are duplicate callbacks handled safely?
- Are refund operations authorized?
- Are state transitions enforced?
- Are balance updates atomic?
- Are unusual payment patterns logged and monitored?
A payment system is not secure just because it uses a trusted payment gateway.
The application integrating that gateway must also enforce its own business rules.
Final Thoughts
These four vulnerabilities demonstrate four different ways in which backend applications can fail.
VulnerabilityFundamental mistakeIDORTrusting a resource identifier without checking authorizationDirectory TraversalTrusting user-controlled filesystem pathsInsecure File UploadTrusting uploaded files without proper validation and isolationPayment Logic FlawsTrusting client-controlled state or failing to enforce business rules
Notice the common theme.
The backend receives information from an untrusted environment.
That information may come from a browser, mobile application, API client, or third-party integration.
Your backend must validate that information and enforce the application’s security rules independently.
A frontend can hide buttons, but it cannot enforce authorization.
A filename can end in .jpg, but that does not prove it is a safe image.
A payment success page can load, but that does not prove money was received.
An order can contain a PAID status, but that does not prove the transition was legitimate.
Good backend engineering is not just about making valid requests work. It is about making invalid, unauthorized, and unexpected requests fail safely.
That is one of the most important differences between an application that works and an application that is secure.
Hope you all like this blog, will be back with part 2 !!







