API-specific vulnerabilities: BOLA, mass assignment, and excessive data exposure
Web Security's broken-access-control lesson covered IDOR for browser-facing resources — one user reaching another user's record by guessing an ID. APIs built for app clients and integrations, not browsers, have their own variations of the same underlying mistake, specific to how JSON request and response bodies actually get handled.
Intermediate
3 min read
BOLA: the same IDOR mistake, API Security's own name for it
Broken Object Level Authorization (BOLA) is the API-world term for exactly the IDOR pattern Web Security's broken-access-control lesson already covered: an endpoint checks that a request is authenticated, but not that the authenticated user actually owns or can access the specific object being requested.
GET /api/v2/invoices/4471 — returns invoice 4471 to ANY logged-in user,
not just the user who actually owns it
This lesson names it separately because it's consistently the single most common finding in real-world API security assessments — API endpoints, especially ones built for mobile apps or partner integrations rather than a browser UI, are disproportionately likely to skip this check, often because a developer assumes "only our own app calls this" and treats that as a substitute for an actual ownership check.
Mass assignment: binding the whole request body to a model, including fields that were never meant to be writable
@app.route("/api/users/<id>", methods=["PATCH"])def update_user(id): user = db.get_user(id) for key, value in request.json.items(): setattr(user, key, value) # blindly applies EVERY field the client sent db.save(user)
A convenient shortcut — taking every field in a request body and applying it directly to a model — becomes a real vulnerability the moment a model has a field that should never be client-writable, like is_admin or account_balance. A request body of {"name": "Alice", "is_admin": true} silently grants admin access, not because the endpoint intended to support that, but because nothing ever told it not to accept that field.
The fix: an explicit allowlist of which fields a given endpoint accepts from the client — name and email, say — with every other field on the model simply ignored regardless of what the request body contains, rather than a denylist trying to remember every field that shouldn't be accepted (the same allowlist-over-blocklist principle from Web Security's field-reference lesson, applied to request-body fields instead of URLs).
Excessive data exposure: returning the whole object and trusting the client to discard the rest
@app.route("/api/users/<id>")def get_user(id): user = db.get_user(id) return jsonify(user.__dict__) # returns EVERY field, including password_hash, internal notes, etc.
Serializing an entire internal model directly into a JSON response — rather than an explicit, minimal response schema listing exactly which fields a given endpoint is meant to expose — routinely leaks fields never intended for the client: password hashes, internal flags, other users' data embedded in a nested relation. "The frontend just doesn't display that field" is not a security boundary — anyone can inspect the raw API response directly, regardless of what a particular frontend chooses to render from it.
Why all three share one root cause
BOLA, mass assignment, and excessive data exposure are all versions of the same underlying mistake: treating a model or a request/response body as an undifferentiated blob, instead of explicitly defining exactly what a given endpoint is allowed to read from a request and exactly what it's allowed to expose in a response. An explicit schema for both directions — validated input fields, an explicit output serializer — closes all three at once, because each one is really just a different symptom of skipping that explicit boundary.