CYBERSECURITYintermediate

API Security Beyond HTTPS: Real Attack Vectors and Real Defenses

A hands-on course on securing APIs against the attacks that actually happen in production. Covers BOLA, mass assignment, rate limiting bypasses, JWT attacks, SSRF, and injection through GraphQL. Each vulnerability is demonstrated with a real exploit and then fixed with production-grade code.

Quality Score
Quality: 90/100

Curriculum

  • The OWASP API Top 10

    Understanding the vulnerabilities that actually get exploited in production APIs
    • What Actually Gets Exploited
  • JWT, OAuth, and Session Security

    Attacking and defending authentication tokens, OAuth flows, and session management
    • Attacking and Defending Authentication
  • Rate Limiting, SSRF, and Injection

    The attacks that developers forget about — rate limiting bypasses, server-side request forgery, and injection through modern APIs
    • The Attacks Developers Forget About
Free PreviewThe OWASP API Top 10 · What Actually Gets Exploited

What Actually Gets Exploited: The OWASP API Top 10 in Practice

Forget theoretical vulnerability lists. This lesson walks through the API vulnerabilities that actually show up in bug bounty reports, breach disclosures, and penetration tests. Every example here is based on real-world patterns.

1. Broken Object Level Authorization (BOLA) — API1:2023

BOLA is the #1 API vulnerability for a reason. It happens when an API endpoint exposes object IDs and fails to verify that the requesting user actually owns or has access to that object.

The Vulnerable Pattern

Consider this endpoint that returns order details:

// VULNERABLE: No authorization check on the order owner
app.get('/api/orders/:orderId', authenticate, async (req, res) => {
  const order = await db.orders.findById(req.params.orderId);
  if (!order) return res.status(404).json({ error: 'Not found' });
  return res.json(order); // Anyone can read any order!
});

The Exploit

An attacker who has a legitimate account (user ID 42, order ID 1001) simply iterates through other order IDs:

# Attacker's own order — works fine
curl -H 'Authorization: Bearer ' https://api.target.com/api/orders/1001

# Someone else's order — also works (this is the bug)
curl -H 'Authorization: Bearer ' https://api.target.com/api/orders/1002
curl -H 'Authorization: Bearer ' https://api.target.com/api/orders/1003

# Automated enumeration
for i in $(seq 1 10000); do
  curl -s -H 'Authorization: Bearer ' \
    https://api.target.com/api/orders/$i | jq '.email,.address,.total' >> leaked_data.txt
done

This attack is trivial to execute and devastating in impact. The attacker gets access to every order in the system — names, addresses, payment details, everything the API returns.

The Defense

// SECURE: Always verify ownership
app.get('/api/orders/:orderId', authenticate, async (req, res) => {
  const order = await db.orders.findOne({
    _id: req.params.orderId,
    userId: req.user.id  // Scoped to the authenticated user
  });
  if (!order) return res.status(404).json({ error: 'Not found' });
  return res.json(order);
});

The fix is deceptively simple: add a userId filter to every query. But in practice, this gets missed constantly — especially in admin endpoints, export features, and report generators.

Architectural defense: Use a middleware pattern that automatically scopes queries:

function scopeToUser(model) {
  return async (req, res, next) => {
    req.scopedQuery = { ...req.query, userId: req.user.id };
    next();
  };
}

2. Broken Authentication — API2:2023

Broken authentication in APIs goes beyond weak passwords. The most common patterns:

Credential Stuffing Without Rate Limiting

# No rate limit on login endpoint
for cred in $(cat credential_dump.txt); do
  user=$(echo $cred | cut -d: -f1)
  pass=$(echo $cred | cut -d: -f2)
  curl -s -X POST https://api.target.com/auth/login \
    -d "{\"email\":\"$user\",\"password\":\"$pass\"}" \
    -H 'Content-Type: application/json' | grep -q 'token' && echo "HIT: $cred"
done

Token That Never Expires

// VULNERABLE: Token has no expiration
const token = jwt.sign({ userId: user.id, role: user.role }, SECRET);
// This token works forever. Steal it once, own the account permanently.

// SECURE: Short-lived access token + refresh token rotation
const accessToken = jwt.sign(
  { userId: user.id, role: user.role },
  SECRET,
  { expiresIn: '15m' }
);
const refreshToken = crypto.randomBytes(32).toString('hex');
await redis.setex(`refresh:${refreshToken}`, 86400, user.id);

3. Excessive Data Exposure — API3:2023

APIs often return entire database objects instead of filtering to only what the client needs.

// VULNERABLE: Returns everything including sensitive fields
app.get('/api/users/:id', async (req, res) => {
  const user = await User.findById(req.params.id);
  res.json(user); // Includes password hash, SSN, internal notes, etc.
});

// SECURE: Explicit field selection
app.get('/api/users/:id', async (req, res) => {
  const user = await User.findById(req.params.id)
    .select('name email avatar createdAt');
  res.json(user);
});

Real-world example: API returns admin fields to regular users

curl -H 'Authorization: Bearer ' \
  https://api.target.com/api/users/me

# Response includes:
# {
#   "id": 42,
#   "email": "user@example.com",
#   "name": "Regular User",
#   "password_hash": "$2b$12$...",        ' \
  -H 'Content-Type: application/json' \
  -d '{"name": "New Name", "bio": "Updated bio"}'

# Mass assignment attack — injecting role field
curl -X PUT https://api.target.com/api/users/me \
  -H 'Authorization: Bearer ' \
  -H 'Content-Type: application/json' \
  -d '{"name": "New Name", "role": "admin", "verified": true, "credits": 999999}'

If the server uses something like User.findByIdAndUpdate(id, req.body), all those fields get written.

The Defense

// VULNERABLE: Spreading entire request body
app.put('/api/users/me', authenticate, async (req, res) => {
  await User.findByIdAndUpdate(req.user.id, req.body); // Never do this
});

// SECURE: Explicit allowlist
app.put('/api/users/me', authenticate, async (req, res) => {
  const allowed = ['name', 'bio', 'avatar', 'timezone'];
  const updates = {};
  for (const field of allowed) {
    if (req.body[field] !== undefined) {
      updates[field] = req.body[field];
    }
  }
  await User.findByIdAndUpdate(req.user.id, updates);
  res.json({ updated: true });
});

Use schema validation (Zod, Joi, etc.) at the API boundary to strip unknown fields before they ever reach your business logic.

const updateProfileSchema = z.object({
  name: z.string().max(100).optional(),
  bio: z.string().max(500).optional(),
  avatar: z.string().url().optional(),
  timezone: z.string().optional(),
}).strict(); // .strict() rejects unknown fields

Key Takeaways

  • BOLA is the #1 API vuln. Always scope database queries to the authenticated user.
  • Authentication is more than passwords. Token expiration, refresh rotation, and rate limiting are non-negotiable.
  • Never return full database objects. Use explicit field selection or serialization layers.
  • Mass assignment is a one-line exploit. Always use allowlists for writable fields.
  • Every one of these vulnerabilities has been found in major production APIs — not because they are hard to fix, but because developers forget to check.
Enroll to access all course content via the MCP API.€25.00
To purchase, have your AI agent call stripe_checkout via MCP — it will generate a payment link for you to approve. Once paid, your agent is automatically enrolled.