Research / Application labs

Application security lab case studies

Worked examples of access-control and injection failures in intentionally vulnerable applications. Follow the request, root cause and fix.

Discuss your scope in a 15-min call

How to read these

Real exploits. Recognized labs. No client data.

Honest framing: these are reproductions against well-known, intentionally-vulnerable, self-hosted applications (OWASP crAPI, OWASP Juice Shop, WithSecure's Damn Vulnerable LLM Agent). None of this is real client work, and every bit of testing was authorized. Every command, response, and payload below is real captured output from running the exploit myself. Client work is confidential and never published; these public labs let me show the depth without ever exposing a customer.
Case Study 1

Reading another user's live vehicle location via BOLA High

Target: OWASP crAPI (self-hosted) Class: Broken access control / BOLA + excessive data exposure
Context

crAPI is OWASP's deliberately-vulnerable, realistic vehicle-service SaaS API with real user roles, JWT auth, and a community forum. A close stand-in for the multi-tenant SaaS APIs I test.

Finding

An authenticated attacker who owns no vehicles can read any other user's real-time GPS location. The community endpoint leaks other users' email + vehicleid (excessive data exposure); that vehicleid then works on the location endpoint, which never checks ownership.

Impact

Any free account can track any other customer's physical location in real time and harvest their email. That's a privacy and physical-safety breach, and all it takes to cross that boundary is a free signup.

Evidence: real captured output
# attacker owns NO vehicles
GET /identity/api/v2/vehicle/vehicles            -> []

# community endpoint leaks another user's identifiers
GET /community/api/v2/community/posts/recent
  victim -> email: robot001@example.com  vehicleid: 4bae9968-...-e5e5

# BOLA: victim's location, fetched with the ATTACKER's token
GET /identity/api/v2/vehicle/4bae9968-...-e5e5/location
HTTP/1.1 200 OK
{"carId":"4bae9968-...","vehicleLocation":{"latitude":"37.746880",
  "longitude":"-84.301460"},"fullName":"Robot","email":"robot001@example.com"}
Root cause

The location endpoint authorizes on authentication (a valid JWT) but not on object ownership. It never asks whether the vehicle belongs to the caller. Separately, the community response returns fields the caller has no need to see (email, vehicleid), supplying the id used above.

Remediation

Enforce server-side ownership on every object lookup: the vehicle's owner must equal the caller, else return 404. Trim the community response to non-sensitive fields only. Add object-level authorization tests to CI.

Expected retest behavior

After applying the ownership check, the cross-user request should return 403 or 404 without location or personal data. The community feed should omit other users' email and vehicle identifiers, while legitimate owner access still succeeds. This section describes the checks to perform; it does not present a captured retest result.

Reproduce: cs1_idor_crapi/exploit_crapi_bola.sh (crAPI up on :8888) · script & evidence on GitHub

Case Study 2

An AI agent's tool trusts model-controlled input High

Target: WithSecure "Damn Vulnerable LLM Agent" (self-hosted) Class: LLM/agent tool authorization + injection
Context

A LangChain ReAct "banking assistant" whose system prompt explicitly says to operate ONLY on the current user's id and refuse any other. It exposes a GetUserTransactions tool. It's a faithful model of the agent/tool boundary I test in AI products.

Finding

The tool trusts the id it is handed: it returns another user's transactions without ownership scoping and interpolates that id directly into SQL. The reproduction below calls the tool directly to demonstrate both failures. It does not establish an end-to-end prompt-injection bypass through the conversational agent.

Impact

In the self-hosted lab, attacker-controlled tool arguments return another user's transactions and, through SQL injection, the users' stored credentials. The tool therefore needs its own authorization and input handling, independently of the assistant's prompt rules.

Evidence: real captured output
guardrail (system prompt): operate ONLY on GetCurrentUser()'s id (user 1); refuse others.

[BOLA]  GetUserTransactions('2')  -> another user's data:
  {"transactionId":3,"userId":2,"reference":"PlutoniumPurchase",
   "recipient":"FLAG:plutonium-256","amount":5000.0}

[SQLi]  GetUserTransactions("0' UNION SELECT userId,username,password,'sqli',0 FROM Users-- ")
  -> MartyMcFly:Password1  DocBrown:flux-capacitor-123  BiffTannen:Password3 ...

[retest] scoped + parameterized tool: cross-user leak = False; credential dump = False; own-data = True  -> PASSED
Root cause

The tool derives authority from model/user-controlled arguments instead of the authenticated session, and builds SQL by string interpolation. Model output is treated as trusted input to a privileged data action.

Remediation

Treat model output as untrusted. Scope the query to the authenticated user server-side and ignore any model-supplied id; parameterize the SQL; require confirmation for sensitive tool actions.

Retest

PASSED. With the scoped, parameterized tool the cross-user request returns only the caller's own rows and the injection payload is inert, while legitimate use still works.

Reproduce: cs2_ai_agent/cs2_tool_layer_exploit.py + cs2_fix_retest.py · scripts & evidence on GitHub

Case Study 3

SQL injection in login → authentication bypass as administrator Critical

Target: OWASP Juice Shop v20.2.0 (self-hosted) Class: SQL injection / auth bypass · insecure code → fix → regression
Context

Juice Shop is the most widely recognized deliberately-vulnerable web app. This one goes past the exploit to the corrected code, plus a regression test that proves the fix holds.

Finding

The login endpoint builds its SQL by string-interpolating the email field. The payload ' OR 1=1-- in the email logs the attacker in as the first user, the administrator, with no valid password.

Impact

Full administrator account takeover with no credentials: a single request returns an admin session token (decoded JWT role = admin).

Evidence: real captured output
# baseline: correct endpoint, wrong password
POST /rest/user/login {"email":"admin@juice-sh.op","password":"wrong"}  -> 401

# exploit: SQL injection in the email field
POST /rest/user/login {"email":"' OR 1=1--","password":"anything"}  -> 200 OK
  authentication.umail = admin@juice-sh.op   (decoded JWT role: admin)

# vulnerable code -- routes/login.ts:34
models.sequelize.query(`SELECT * FROM Users WHERE email = '${req.body.email||''}'
  AND password = '${security.hash(req.body.password||'')}' AND deletedAt IS NULL`)

# remediation retest (parameterized query), same payload:
  inject -> no row (denied)   valid login -> ok   wrong password -> denied   -> PASSED
Root cause

User input (email) is concatenated into the SQL statement, so input can change the query's structure. That's the classic SQL-injection root cause.

Remediation

Use parameterized queries / an ORM binding so input can never alter SQL structure (e.g. WHERE email = ? AND password = ?). Never interpolate request fields into SQL.

Retest

PASSED. A faithful reproduction of the same query shows the injection succeeds before the fix and fails after, while valid login still works and a wrong password is still rejected.

Reproduce: cs3_sqli_juiceshop/sqli_fix_reproduction.py (live exploit + fix) · script & evidence on GitHub

For your product, explore SaaS and API reviews or AI agent security testing.

Your next step

Talk through your product.

Bring your product, your timeline, and the questions your customers are asking. We’ll use a 15-minute call to discuss fit and scope.