DEV Community

Cover image for Your API is the door. Most people forget to lock it.
Mr Recruiter
Mr Recruiter

Posted on

Your API is the door. Most people forget to lock it.

Here's the thing about APIs that makes them uniquely dangerous: they're built to be talked to. A database can hide behind a private network. A server can sit in a locked-down subnet. But an API's entire job is to be reachable and to respond to requests, which means it's the one part of your system you deliberately expose, and therefore the one an attacker gets to poke at all day. So the security of your API isn't a nice-to-have layer on top. It's the door, and the door is standing open by design.

Let me walk through where API security actually goes wrong, because it's rarely the exotic stuff.

Authentication and authorization are different, and the second one is where you'll get breached

People conflate these constantly. Authentication is "who are you." Authorization is "are you allowed to do this specific thing." Most teams get authentication roughly right, there's a login, there's a token. Then they under-do authorization, and that's where the actual breaches live.

The classic failure: an API checks that you're a valid, logged-in user, and then trusts you to only ask for your own data. So the request comes in for /users/123/orders, the API confirms you're logged in, and hands over user 123's orders, without checking whether you are user 123. Change the number to 124 and you get someone else's data. This is called broken object level authorization, it's one of the most common API vulnerabilities in existence, and it comes down to authenticating the user but never authorizing them against the specific resource they asked for. Every endpoint that returns a specific object needs to check not just "are you logged in" but "are you allowed to see this exact thing."

Never trust input, because the client is not on your side

Anything coming from the client is attacker-controlled, full stop. Even if you built the frontend, even if your app "would never send that," an attacker isn't using your frontend, they're hitting your API directly with whatever they want. So every input has to be validated on the server, server-side, always, not just in the UI.

Client-side validation is for user experience, catching typos before a round trip. It is worthless as security, because the attacker skips your client entirely. Validate types, ranges, formats, and lengths on the server. Reject what doesn't fit rather than trying to sanitize it into shape. And be especially careful with anything that flows into a query, a command, or a template, because unvalidated input reaching those is how injection attacks happen.

Rate limiting isn't optional, it's a security control

An API with no rate limiting is an open invitation. Without it, an attacker can hammer your login endpoint with thousands of password guesses, scrape your entire dataset one request at a time, or just knock your service over with volume. Rate limiting, capping how many requests a given client can make in a window, is what turns those attacks from trivial into impractical.

It's easy to think of rate limiting as purely a performance or cost thing. It's also genuinely a security control, because a lot of attacks depend on being able to make a huge number of requests, and taking that ability away defangs them. Brute-forcing a token is only feasible if you can try millions of times.

Don't leak information in errors and responses

APIs love to be helpful, and helpful is dangerous. Verbose error messages that reveal your stack, your database structure, or internal paths are handing an attacker a map. Responses that return more fields than the client needs, including internal flags, other users' metadata, or sensitive attributes, leak data you never meant to expose. Return only what's needed, and keep errors generic to the outside world while logging the detail internally. "Something went wrong" to the client, the full stack trace to your logs, never the other way around.

Every endpoint is attack surface, including the ones you forgot

Here's a quiet killer: the old, undocumented, or forgotten endpoints. The v1 API you never turned off. The debug endpoint someone left in. The internal endpoint that's actually reachable. Attackers find these, and they're often less protected than your current, watched endpoints because nobody's thinking about them. Your attack surface is every endpoint that responds, not just the ones in your current docs. Know what you're exposing, and turn off what you're not deliberately maintaining.

Treat tokens and keys like the credentials they are

API keys and tokens are credentials, and they leak constantly, committed to repos, logged in plaintext, embedded in client-side code where anyone can read them. Never put a secret in client-side code, because "client-side" means "publicly readable." Keep tokens scoped to the minimum they need, give them expiry so a leaked one doesn't work forever, and rotate them. A long-lived, over-permissioned key that leaked two years ago and still works is exactly the kind of thing that shows up in breach post-mortems.

The mental model

Your API is the deliberately-exposed surface of your whole system, so assume it's under constant hostile inspection, because it is. Authorize every request against the specific resource, not just the identity. Trust no input from the client. Rate limit as a security measure. Leak nothing in errors or over-fetched responses. Know every endpoint you expose. And treat every token as a credential that will eventually leak. None of it is exotic. It's just the discipline of remembering that the door is open on purpose, and acting accordingly.

The API being reachable is the whole point of it. That same reachability is why it's the first thing an attacker tries. Build like both are true, because they are.

Top comments (0)