Security Basics

9 min

API Security for Startups Under Real Attack

API Security for Startups Under Real Attack

Most API breaches aren't dramatic. They start with a predictable request, a missing ownership check, and data that was never meant to come back. The failures attackers test first, and how to check for them.

Most API breaches aren't dramatic. They start with a predictable request, a missing ownership check, and data that was never meant to come back. The failures attackers test first, and how to check for them.

A customer should not be able to change one number in an API request and read another customer’s invoice. Yet that is exactly how many API security failures begin: not with a dramatic breach, but with a predictable request, a missing ownership check, and data that was never meant to be returned.

For founders shipping quickly with AI-assisted code, hosted authentication and managed databases, APIs are often the part of the product doing the most sensitive work. They create accounts, return customer records, trigger payments, update permissions and connect to third-party services. If an attacker can call those endpoints outside the intended user interface, they can test every assumption the application makes about who they are and what they should be allowed to do.

Why API security is a business risk, not a back-end detail

Your front end may hide an admin button, omit a field from a dashboard or prevent a user from selecting another account. None of that protects the underlying API if the endpoint accepts a direct request without enforcing the same rule server-side.

Attackers do not need to understand your entire application. They need to find one useful route: an endpoint that exposes records by ID, an update action that trusts a client-supplied user_id, a webhook that accepts forged events, or a database policy that lets an authenticated user query far more than their own data. From there, the consequences are practical and immediate: customer data exposure, account takeover, fraudulent refunds, manipulated marketplace listings or unexpected infrastructure costs.

This is especially common in rapidly assembled SaaS products. AI-generated code can produce convincing handlers, forms and database queries, but it may copy a pattern without understanding the permission model around it. A route can look complete in a code review while missing the one condition that prevents cross-account access.

The question is not whether your product has an API. If it uses authentication, a database, payments, file uploads or client-side data fetching, it almost certainly does. The question is whether every route makes an explicit decision about identity, authority and the data it returns.

The API security failures attackers test first

Authentication that proves too little

Authentication answers, “Who is making this request?” A valid session, cookie or JWT can establish an identity. It does not establish that the person is allowed to perform the requested action.

Problems appear when applications accept unsigned JWTs, fail to validate issuer or audience claims, use weak session handling, or treat a token from one environment as valid in another. More often, the token is valid but the route assumes that any signed-in user can access the requested record.

A sound endpoint verifies the token on the server, derives the current user from that verification, and rejects absent, expired or malformed credentials. Do not rely on a user ID supplied in the request body when the authenticated identity is already available. Client input is a request, not a source of authority.

Broken authorisation and IDOR

Insecure direct object references, usually called IDOR, are among the highest-value findings in a startup application. They occur when an endpoint accepts a predictable identifier such as /api/orders/1842 and returns, changes or deletes that object without checking that it belongs to the current user or organisation.

The identifier does not need to be sequential for this to matter. UUIDs reduce guessability, but they do not replace authorisation. A user who sees another ID in a response, URL, log entry or shared link may still be able to use it against an endpoint that never checks ownership.

For every route that reads or modifies a record, ask two separate questions: does this requester have a valid identity, and does that identity have permission for this exact object? In multi-tenant products, the second check often needs to confirm both individual ownership and organisation membership. Role checks alone are not enough if an administrator from Company A can alter Company B’s data.

Database exposure hidden behind Supabase

Supabase can accelerate a product dramatically, but its row-level security model must match the way your application actually accesses data. A table with RLS disabled may be queryable through the data API by anyone holding a usable key. A table with RLS enabled can still be exposed by an overly broad policy such as allowing all authenticated users to select all rows.

Policies should be reviewed table by table and action by action. A customer may need to read their own subscription but not update its status. A seller may need to create a listing but not alter another seller’s inventory. Service processes may need elevated access, but that access belongs only in a trusted server environment.

A Supabase service-role key is not a browser configuration value. If it appears in a JavaScript bundle, public repository, build log or client-side environment variable, assume it has been exposed. Rotate it, remove it from the client and investigate what it could access. The same rule applies to payment secrets, private API tokens and database connection strings.

Payment and webhook routes that trust the caller

Payment flows introduce an additional mistake: treating a browser redirect or client-provided payment status as proof that money changed hands. The server should verify payment state with the provider or rely on a properly validated webhook. It should not mark an order as paid because the client sent { "status": "paid" }.

Webhook handlers need signature verification, timestamp or replay protection where supported, and idempotency. Without idempotency, a legitimate event delivered twice can create duplicate credits, fulfilments or refunds. Without signature verification, an attacker may be able to imitate the provider and trigger the same business logic directly.

Excessive data and missing request controls

An endpoint can enforce ownership and still disclose too much. Returning an entire user object may accidentally include internal roles, billing references, password-reset metadata or third-party identifiers that were never needed by the screen calling it. Shape responses deliberately. Return the fields required for the job, not the whole database row because it is convenient.

Rate limits also matter where requests have a financial, security or compute cost. Login, password reset, verification, search, AI generation, exports and payment actions all deserve specific limits. A single global limit is better than none, but endpoint-specific controls are more useful because normal usage varies sharply between routes.

How to assess an API before it becomes an incident

Start with the live application, not only the repository. Browser network requests reveal routes that are easy to miss in source code, including backend functions, storage URLs and third-party integrations. Test what an ordinary user can see, then repeat the same requests with changed IDs, changed organisation references, missing tokens and lower-privileged accounts.

Next, trace sensitive actions from the route to the database. Identify where the endpoint gets the current user, where it decides access is allowed, and what query it finally executes. If a query filters on an ID from the request but never scopes it to the authenticated user or tenant, it deserves immediate attention.

Review configuration alongside code. Check exposed environment variables, repository history, deployment settings, Supabase RLS policies, CORS rules, security headers and dependency alerts. An application can have carefully written API handlers while a leaked credential or permissive database policy bypasses them entirely.

Automated tooling helps find patterns at speed. Secret scanning can identify credentials in commits; dependency scanning can flag known vulnerable packages; static analysis can surface dangerous code paths; and dynamic testing can probe live endpoints. But automated output is not the final decision. Manual review is where someone follows the product’s actual permission model, tests realistic abuse paths and distinguishes a theoretical warning from a route that lets one customer access another customer’s data.

Fix the highest-risk paths first

Not every finding deserves the same response. Prioritise by what an attacker can do now, how easily they can do it, and what data or money is involved. A publicly exposed service-role key, an IDOR on customer records or an unsigned JWT acceptance issue should move ahead of a low-impact header improvement.

For critical routes, fixes are usually specific. Add a server-side ownership check before the query. Restrict a Supabase policy to auth.uid(). Move a privileged key into server-only configuration and rotate it. Verify the webhook signature before processing the payload. Return a reduced response object. Add a rate limit around a costly action.

Then test the fix as an attacker would. Sign in as two separate users. Create records under each account. Try to read, edit and delete across the boundary. Repeat with no session, an expired session and a modified request body. Security controls are only useful when they fail closed under the conditions an attacker will actually try.

HollowByte reviews these paths across the live application and repository, then reports the exact affected route, file or configuration, severity and remediation work required. That approach matters when a small team needs to decide what to fix this week rather than collect a long list of unranked warnings.

The most useful next step is simple: choose the endpoint that can cause the most damage if misused - payments, customer records, admin actions or account settings - and test it from an account that should not have access. If that request succeeds, you have found a priority. If you cannot confidently explain why it fails, it is time to inspect the enforcement behind it.

Back to blog

(function() { function applyMainRole() { var hero = document.getElementById('hero'); if (!hero) return false; var node = hero; while (node.parentElement && node.parentElement !== document.body) { node = node.parentElement; } if (node && node.parentElement === document.body) { node.setAttribute('role', 'main'); return true; } return false; } if (applyMainRole()) return; var attempts = 0; var interval = setInterval(function() { attempts++; if (applyMainRole() || attempts > 20) { clearInterval(interval); } }, 250); })();