OWASP API - Top Ten

OWASP Top 10 API Security Risks

Source: https://owasp.org

According to OWASP, the following are the top 10 API security risks:

API1: Broken Object-Level Authorization

The Problem: You're not checking if users can access specific objects/resources.

Bad Code Example:

// BAD - No authorization check
app.get("/api/users/:userId/profile", (req, res) => {
  const profile = getUserProfile(req.params.userId);
  res.json(profile);
});

Good Code Example:

// GOOD - Check if authenticated user can access this profile
app.get("/api/users/:userId/profile", authenticateUser, (req, res) => {
  const requestedUserId = req.params.userId;
  const currentUserId = req.user.id;

  // Only allow users to access their own profile
  if (requestedUserId !== currentUserId && !req.user.isAdmin) {
    return res.status(403).json({ error: "Forbidden" });
  }

  const profile = getUserProfile(requestedUserId);
  res.json(profile);
});

API2: Broken Authentication

The Problem: Weak authentication implementation allows token theft or identity spoofing.

Common Mistakes:

Better Approach:

// Use httpOnly cookies for tokens
app.post("/login", (req, res) => {
  // Validate credentials...
  const token = jwt.sign({ userId }, process.env.JWT_SECRET, {
    expiresIn: "15m",
  });

  res.cookie("token", token, {
    httpOnly: true,
    secure: true, // HTTPS only
    sameSite: "strict",
    maxAge: 15 * 60 * 1000, // 15 minutes
  });
});

API3: Broken Object Property Level Authorization

The Problem: Exposing sensitive fields that users shouldn't see.

Bad Code:

// BAD - Exposing everything
app.get("/api/users/:id", (req, res) => {
  const user = database.users.findById(req.params.id);
  res.json(user); // Includes password, ssn, internal flags, etc.
});

Good Code:

// GOOD - Filter sensitive data based on user role
app.get("/api/users/:id", (req, res) => {
  const user = database.users.findById(req.params.id);

  const publicFields = {
    id: user.id,
    name: user.name,
    email: user.email,
  };

  // Only admins see sensitive data
  if (req.user.role === "admin") {
    publicFields.ssn = user.ssn;
    publicFields.internalNotes = user.internalNotes;
  }

  res.json(publicFields);
});

API4: Unrestricted Resource Consumption

The Problem: No rate limiting allows DoS attacks.

Solution - Implement Rate Limiting:

const rateLimit = require("express-rate-limit");

// Basic rate limiting
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: "Too many requests from this IP",
});

// Expensive operation needs stricter limits
const searchLimiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 5, // only 5 searches per minute
});

app.use("/api/", limiter);
app.use("/api/search", searchLimiter);

API5: Broken Function-Level Authorization

The Problem: Users accessing admin functions they shouldn't.

Bad Code:

// BAD - Only checking authentication, not authorization
app.delete("/api/users/:id", authenticateUser, (req, res) => {
  deleteUser(req.params.id);
  res.json({ success: true });
});

Good Code:

// GOOD - Check specific permissions
const requireRole = (role) => (req, res, next) => {
  if (req.user.role !== role) {
    return res.status(403).json({ error: "Insufficient permissions" });
  }
  next();
};

app.delete(
  "/api/users/:id",
  authenticateUser,
  requireRole("admin"),
  (req, res) => {
    deleteUser(req.params.id);
    res.json({ success: true });
  },
);

API6: Unrestricted Access to Sensitive Business Flows

The Problem: No protection against automated abuse of business processes.

Example Solution:

// Protect ticket purchasing from bots
const ticketPurchaseLimiter = rateLimit({
  windowMs: 5 * 60 * 1000, // 5 minutes
  max: 2, // max 2 ticket purchases per 5 minutes
  keyGenerator: (req) => req.user.id, // per user, not per IP
});

app.post(
  "/api/tickets/purchase",
  authenticateUser,
  ticketPurchaseLimiter,
  validateCaptcha, // additional bot protection
  (req, res) => {
    // Purchase logic here
  },
);

API7: Server-Side Request Forgery

The Problem: API makes requests to URLs provided by users.

Bad Code:

// BAD - Directly using user-provided URL
app.post("/api/fetch-image", (req, res) => {
  const imageUrl = req.body.url;
  fetch(imageUrl).then((response) => {
    // Process image...
  });
});

Good Code:

// GOOD - Validate and restrict URLs
app.post("/api/fetch-image", (req, res) => {
  const imageUrl = req.body.url;

  // Validate URL format
  if (!isValidHttpUrl(imageUrl)) {
    return res.status(400).json({ error: "Invalid URL" });
  }

  // Block internal/private networks
  const url = new URL(imageUrl);
  if (isPrivateIP(url.hostname) || url.hostname === "localhost") {
    return res
      .status(400)
      .json({ error: "Access to private networks forbidden" });
  }

  // Only allow specific domains
  const allowedDomains = ["cdn.example.com", "images.trusted-site.com"];
  if (!allowedDomains.includes(url.hostname)) {
    return res.status(400).json({ error: "Domain not allowed" });
  }

  fetch(imageUrl).then((response) => {
    // Process image...
  });
});

API8: Security Misconfiguration

The Problem: Default configs, unnecessary services, verbose errors.

Key Dev Practices:

// Hide framework details
app.disable("x-powered-by");

// Proper error handling - don't leak stack traces
app.use((err, req, res, next) => {
  console.error(err.stack); // Log for debugging

  // Don't send stack traces to clients
  res.status(500).json({
    error:
      process.env.NODE_ENV === "production"
        ? "Internal server error"
        : err.message,
  });
});

// Security headers
app.use((req, res, next) => {
  res.setHeader("X-Content-Type-Options", "nosniff");
  res.setHeader("X-Frame-Options", "DENY");
  res.setHeader("X-XSS-Protection", "1; mode=block");
  next();
});

API9: Improper Inventory Management

The Problem: Old API versions still running with weaker security.

Dev Best Practices:

// Version-specific middleware
app.use("/api/v1", (req, res, next) => {
  res.setHeader("Sunset", "Sat, 31 Dec 2023 23:59:59 GMT");
  res.setHeader("Deprecation", "true");
  next();
});

API10: Unsafe Consumption of APIs

The Problem: Trusting third-party API data too much.

Bad Practice:

// BAD - Trusting third-party data
app.get("/api/weather", async (req, res) => {
  const weatherData = await fetch("https://weather-api.com/current");
  const data = await weatherData.json();

  // Directly using unvalidated data
  res.json(data);
});

Good Practice:

// GOOD - Validate and sanitize third-party data
app.get("/api/weather", async (req, res) => {
  try {
    const weatherData = await fetch("https://weather-api.com/current");
    const rawData = await weatherData.json();

    // Validate the structure and content
    const validatedData = {
      temperature: typeof rawData.temp === "number" ? rawData.temp : null,
      condition:
        typeof rawData.condition === "string"
          ? sanitizeString(rawData.condition)
          : "Unknown",
      // Only include fields you expect and validate
    };

    res.json(validatedData);
  } catch (error) {
    res.status(503).json({ error: "Weather service unavailable" });
  }
});

Quick Developer Checklist:

  1. ✅ Always check user permissions for specific resources
  2. ✅ Use secure authentication patterns (httpOnly cookies, strong secrets)
  3. ✅ Filter response data based on user roles
  4. ✅ Implement rate limiting on all endpoints
  5. ✅ Validate user roles before allowing actions
  6. ✅ Add business logic protection (captcha, limits)
  7. ✅ Never trust user-provided URLs
  8. ✅ Remove default configs and verbose errors
  9. ✅ Maintain API version inventory
  10. ✅ Validate all third-party API data