Skip to main content
Back to module
Back-End Web APIs · Auth and Security

API security basics

Reading29mLesson 3 of 3 in this module

Recognise the most common API flaws from the OWASP API Security Top 10 and fix broken access control and injection.

Why it matters

An API is a public front door to your data. Unlike a web page, it has no buttons that hide what a user "should not" click: anyone can send any request with any ID. The OWASP API Security Top 10 (from the Open Worldwide Application Security Project) lists the most common ways APIs get broken. The two at the top of most real-world incident reports are simple: broken access control and injection. Both are bugs in ordinary route code, and both are easy to prevent once you know the pattern.

Broken object level authorization (BOLA)

The first entry in the OWASP API Top 10 is Broken Object Level Authorization. It looks like this:

// BUG: any logged-in user can read any order
app.get("/orders/:id", requireLogin, async (req, res) => {
  const order = await db.orders.findById(req.params.id);
  if (!order) return res.status(404).json({ error: "Not found." });
  res.json(order);
});

requireLogin proves who the caller is. Nothing checks that the order belongs to them. An attacker logs in with their own account and changes /orders/1001 to /orders/1000, /orders/999, and so on. Sequential IDs make it trivial, but random IDs are not a fix on their own; IDs leak through URLs, logs and shared links.

The fix is to check ownership on every request that names an object:

app.get("/orders/:id", requireLogin, async (req, res) => {
  const order = await db.orders.findById(req.params.id);
  const allowed = order && (order.userId === req.user.id || req.user.role === "admin");
  if (!allowed) return res.status(404).json({ error: "Not found." });
  res.json(order);
});

Even better, put the owner in the query itself so the database cannot return someone else's row: WHERE id = ? AND user_id = ?. Returning 404 (not 403) for other people's objects avoids confirming that the ID exists.

Related entries in the list:

  • Broken function level authorization: a student can call DELETE /admin/users/7 because only the admin page was hidden, not the route.
  • Broken object property level authorization: a user can update role or isVerified through PATCH /me (mass assignment), or a response includes fields such as passwordHash that the caller should never see. Allow-list the fields you accept and the fields you return.

Injection

Injection happens when untrusted input is mixed into code or a query that another system then runs. SQL injection is the classic example:

// BUG: input becomes part of the SQL text
const sql = `SELECT id, title FROM posts WHERE author = '${req.query.author}'`;

The fix is a parameterised query: keep the SQL text fixed and pass the value separately.

const sql = "SELECT id, title FROM posts WHERE author = ?";
const rows = db.prepare(sql).all(req.query.author);

The same pattern applies elsewhere:

  • NoSQL injection: in MongoDB, a body of { "email": { "$ne": null } } turns an equality check into "any email". Validate that fields are plain strings before querying.
  • Command injection: never build shell commands from input (exec("convert " + filename)). Use APIs that take an argument array, or avoid the shell entirely.
  • Cross-site scripting (XSS): user text rendered into HTML without escaping. APIs should return data as JSON and let the front end render it safely.

Other entries worth knowing

  • Broken authentication: weak login flows, no rate limit on password or code guessing, tokens that never expire.
  • Unrestricted resource consumption: no limits on page size, upload size or request rate, so one caller can exhaust your server or your bill. Cap ?limit=, body size and requests per minute.
  • Security misconfiguration: verbose error pages with stack traces, permissive CORS (Access-Control-Allow-Origin: * with credentials), default credentials, debug endpoints left on.
  • Server-side request forgery (SSRF): an endpoint that fetches a URL the caller supplies can be pointed at internal addresses. Allow-list destinations.
  • Improper inventory management: forgotten old versions (/v1/) and test endpoints still running without the newer fixes.

The full list, with examples, is on the OWASP API Security Project site (checked on 2026-10-23). The list is revised every few years, so read the current edition.

A habit, not a checklist

Most of these bugs come from asking the wrong question. For every route, ask:

  1. Who is calling? (authentication)
  2. May they do this action? (function-level authorization)
  3. May they touch this specific object and these fields? (object and property authorization)
  4. Is every input validated and passed to other systems as data, not code? (validation and injection)
  5. What stops someone from doing this a million times? (limits)

Write a test for each answer, especially the "other user's ID" case.

Common mistakes

  • Hiding a button in the UI and assuming the route is protected.
  • Checking login but not ownership.
  • Building SQL, shell commands or Mongo filters from raw input.
  • Returning whole database rows instead of an allow-listed set of fields.
  • No limits on list sizes, uploads or request rates.

Quick recap

  • Broken access control is the most common API flaw: check ownership and role on every object, ideally in the query.
  • Injection is prevented by keeping code and data separate: parameterised queries, validated types, no shell strings.
  • Allow-list input fields and output fields; cap sizes and rates.
  • Read the current OWASP API Security Top 10 and test the "someone else's ID" case for every route.

Try it: the Do tab gives you two vulnerable functions. Fix the injection and the access-control bug so the checks pass.

Also in this lesson: