Securing Your REST/GraphQL APIs — Essential Best Practices
For six months, I tested several vulnerable environments (CRAPI, VAPI, DVGA) and helped development teams secure their APIs. Here is a summary of the most frequent vulnerabilities — and above all how to fix them.
The API Risk Landscape
The OWASP API Security Top 10 remains the reference. The three flaws I encounter most often:
| # | Vulnerability | Frequency |
|---|---|---|
| 1 | Broken Object Level Authorization (BOLA) | 🔴 Very high |
| 2 | Injection (SQL, NoSQL, command) | 🔴 High |
| 3 | Broken Authentication | 🟡 Medium |
1. BOLA — Broken Object Level Authorization
The problem
A user accesses /api/orders/123 — but nothing verifies that they actually own order 123. They just need to change 123 to 124, 125… to read other people’s data.
This is the API equivalent of web IDOR. It is the most common vulnerability in APIs.
Demo (CRAPI style)
GET /api/orders/1001 HTTP/1.1
Authorization: Bearer <token_user_A>
→ Response: order 1001 data (belonging to user_A). ✅
GET /api/orders/1002 HTTP/1.1
Authorization: Bearer <token_user_A>
→ Response: order 1002 data (belonging to user_B). ❌ BOLA!
The fix
Always verify ownership server-side — never trust the client.
// ❌ BAD
app.get('/api/orders/:id', auth, async (req, res) => {
const order = await Order.findById(req.params.id);
res.json(order);
});
// ✅ GOOD
app.get('/api/orders/:id', auth, async (req, res) => {
const order = await Order.findOne({
_id: req.params.id,
userId: req.user.id, // ownership check
});
if (!order) return res.status(404).json({ error: 'Not found' });
res.json(order);
});
2. Injections
SQL Injection
Always use parameterized queries:
// ❌ BAD — concatenation
const q = `SELECT * FROM users WHERE name = '${name}'`;
// ✅ GOOD — parameterized
const q = 'SELECT * FROM users WHERE name = $1';
db.query(q, [name]);
NoSQL Injection (MongoDB)
Registration: {"username": {"$ne": null}} can match all users!
// ❌ BAD
User.find({ username: req.body.username });
// ✅ GOOD — validate the type
if (typeof req.body.username !== 'string') {
return res.status(400).json({ error: 'Invalid' });
}
GraphQL — Depth & Complexity
On GraphQL (DVGA demo), depth and complexity attacks can DoS the server:
# Recursive bomb
query { user { friends { user { friends { user { ... } } } } } }
Fix: limit query depth and cost.
import depthLimit from 'graphql-depth-limit';
import costAnalysis from 'graphql-cost-analysis';
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(5),
costAnalysis({ maximumCost: 1000 }),
],
});
3. Weak Authentication
JWT — classic pitfalls
// ❌ NEVER DO THIS
const decoded = jwt.decode(token);
if (decoded.admin) { /* grant admin */ }
// ✅ ALWAYS VERIFY THE SIGNATURE
const decoded = jwt.verify(token, SECRET);
Mistakes I often see:
alg: noneaccepted- Weak shared secret
- JWT without expiration
- Non-rotating refresh token
Rate limiting
Without rate limiting, brute-force is guaranteed:
import rateLimit from 'express-rate-limit';
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
message: 'Too many attempts',
});
app.post('/api/login', loginLimiter, loginHandler);
4. Mass Assignment
A user sends an unexpected field (role: 'admin') and your code does User.create(req.body).
// ❌ BAD
const user = await User.create(req.body);
// ✅ GOOD — whitelist
const { username, email, password } = req.body;
const user = await User.create({ username, email, password });
5. Misconfigured CORS
// ❌ DANGER
app.use(cors({ origin: '*' }));
// ✅ SAFE
app.use(cors({
origin: ['https://app.mydomain.com'],
credentials: true,
}));
End-of-development checklist
Before putting an API into production:
- Authorization verified on every sensitive endpoint
- Parameterized queries everywhere (zero SQL concatenation)
- Input validation (zod, joi, class-validator)
- Rate limiting on login, register, reset password
- JWT with strong secret, short expiration, refresh rotation
- HTTPS mandatory, HSTS enabled
- Security logs (auth failures, denied access)
- Restrictive CORS
- Headers: CSP, X-Content-Type-Options, X-Frame-Options
- Testing with tools (Burp, OWASP ZAP, nuclei)
Tools I recommend
| Tool | Usage |
|---|---|
| Burp Suite | Proxy, scanner, intruder |
| OWASP ZAP | Free scanner, CI/CD automation |
| nuclei | CVE templates, mass detection |
| ffuf | Endpoint and parameter fuzzing |
| graphql-playground | GraphQL introspection and testing |
| jwt_tool | JWT analysis and forging |
Conclusion
90% of the API vulnerabilities I find come from a broken authorization model (BOLA) or missing input validation. Covering these two points properly eliminates most of the risk.
API security is not an “extra” topic — it is an axis to integrate from the design phase, and to test continuously via automated scans in your CI/CD pipelines.
“A secure API is not an API that does nothing. It is an API that refuses well.”