DEV Community

Cover image for Your API Is Public by Default — Let’s Fix That
Frozen Blood
Frozen Blood

Posted on

Your API Is Public by Default — Let’s Fix That

Image

Image

Image

Image

Here’s a scary thought: every API you deploy is public unless you actively make it private.
Not “public” as in Google-indexed — public as in reachable, callable, and attackable.

Most backend breaches don’t come from elite hackers. They come from bored scripts, leaked tokens, or endpoints you forgot existed. Let’s walk through the most common API security mistakes I still see in production—and how to harden your backend without turning it into a usability nightmare.


1. “We Have Auth” Is Not a Security Strategy

Authentication answers who you are.
Authorization answers what you’re allowed to do.

Many APIs stop at auth.

GET /api/users/123
Authorization: Bearer <valid-token>
Enter fullscreen mode Exit fullscreen mode

If the token is valid, the request succeeds — even if the user shouldn’t see that data.

The fix: enforce ownership and roles everywhere

// ❌ Only checks auth
if (!req.user) throw new UnauthorizedError();

// ✅ Checks authorization
if (req.user.id !== params.userId && !req.user.isAdmin) {
  throw new ForbiddenError();
}
Enter fullscreen mode Exit fullscreen mode

Security rule of thumb:
Every read, write, and delete must answer “why is this user allowed?”


2. JWTs Are Not Magic Shields

JWTs are great. Misused JWTs are dangerous.

Common issues:

  • No expiration (exp)
  • Tokens stored in localStorage
  • Tokens accepted forever after user logout
  • No audience (aud) or issuer (iss) validation

Minimum safe JWT setup

const payload = jwt.verify(token, JWT_SECRET, {
  audience: 'api.myapp.com',
  issuer: 'auth.myapp.com',
});
Enter fullscreen mode Exit fullscreen mode

Also:

  • Short-lived access tokens (5–15 min)
  • Refresh tokens stored httpOnly
  • Token rotation on refresh

JWTs don’t make you secure — your validation logic does.


3. Rate Limiting Isn’t Optional Anymore

If your API has:

  • Login
  • OTP
  • Password reset
  • Search
  • Public endpoints

…it needs rate limiting. Period.

Without it:

  • Brute-force attacks are trivial
  • Scrapers will eat your bandwidth
  • One bad client can DOS your system

Simple rate limit example

import rateLimit from 'express-rate-limit';

export const limiter = rateLimit({
  windowMs: 60 * 1000,
  max: 100, // requests per minute
});
Enter fullscreen mode Exit fullscreen mode

Apply different limits for:

  • Auth endpoints
  • Public APIs
  • Internal services

Security isn’t about blocking users — it’s about controlling abuse.


4. Overexposed Data (aka Accidental Leaks)

This is the most common real-world breach I see.

Example:

{
  "id": 42,
  "email": "user@example.com",
  "passwordHash": "...",
  "isAdmin": false,
  "createdAt": "..."
}
Enter fullscreen mode Exit fullscreen mode

Nobody meant to expose passwordHash.
It just happened.

Fix: explicit response contracts

// ✅ Safe DTO
return {
  id: user.id,
  email: user.email,
  createdAt: user.createdAt,
};
Enter fullscreen mode Exit fullscreen mode

Never rely on:

  • ORM default serialization
  • res.json(entity)
  • “We’ll filter it on the frontend”

If you didn’t whitelist it, it shouldn’t leave the server.


5. CORS Is Not an Auth Mechanism

I still hear:

“It’s safe, our CORS is locked down.”

CORS only affects browsers.
Attackers don’t use browsers.

curl https://api.yoursite.com/secret
Enter fullscreen mode Exit fullscreen mode

CORS doesn’t stop:

  • Server-to-server calls
  • Bots
  • Mobile apps
  • Postman
  • Curl

What CORS is for

  • Preventing malicious websites from abusing your users’ browsers

What it’s not for

  • Protecting your API

You still need auth, authorization, and rate limiting.


6. Secrets in the Wrong Places

If your repo ever contained:

  • .env files
  • API keys
  • Firebase configs
  • AWS credentials

…assume they’re compromised.

Rules that save careers:

  • Secrets only in environment variables
  • Rotate keys regularly
  • Never log secrets (even in debug)
  • Scope keys to the minimum permissions

If a key leaks, your blast radius should be tiny, not existential.


7. Missing Audit Trails

When something goes wrong, you need answers:

  • Who did this?
  • When?
  • From where?
  • Using which token?

If you don’t log security-relevant actions, you’re blind.

Log:

  • Auth attempts
  • Permission failures
  • Admin actions
  • Token refreshes
  • Role changes

Not verbosely. Intentionally.


8. “Internal” APIs Are Still APIs

Microservices make this worse.

Just because an endpoint is:

  • Behind a VPC
  • On a private subnet
  • “Only called by services”

…doesn’t mean it’s safe.

Protect internal APIs with:

  • Service-to-service auth
  • Short-lived tokens
  • Network-level rules
  • Explicit permissions

Zero trust isn’t paranoia — it’s realism.


Key Takeaway

API security isn’t one feature.
It’s a collection of boring, disciplined decisions:

  • Explicit authorization
  • Minimal data exposure
  • Rate limits everywhere
  • Short-lived credentials
  • Clear audit logs

Most breaches don’t come from sophisticated exploits.
They come from defaults you forgot to change.

Top comments (1)

Collapse
 
martijn_assie_12a2d3b1833 profile image
Martijn Assie

Strong write-up, very grounded.
I like how this focuses on boring defaults instead of hypothetical attackers... that’s where things actually go wrong.
Clear examples, practical fixes, no fear-mongering, just solid backend hygiene.
This is the kind of post people should read before shipping, not after an incident.