Mapping Juice Shop
By the end of this lesson you will be able to build a complete map of a web application: its pages, its API endpoints, the inputs each one accepts, and the "hidden" places nobody links to. You will practise on OWASP Juice Shop in your lab and use Python to turn raw history into a clean inventory.
The Juice Shop project's own guide gives good advice here: understand how the application works before you test it, and never start by blindly throwing attack payloads at it. Mapping is how you build that understanding.
Why map first
Imagine a new delivery rider in Jaipur. Before the first rush hour, they ride the area once: which lanes are one-way, which buildings have back gates, which shops close early. That one slow ride saves hours of confusion later. Mapping a web app is the slow first ride.
A good map answers four questions:
- What pages does the app have?
- What API endpoints do those pages call?
- What inputs does each endpoint accept (parameters, JSON fields, headers, files)?
- What exists but is not linked from anywhere?
This lesson maps to WSTG's Information Gathering tests, such as reviewing web server metafiles and mapping execution paths. See Penetration Testing Methodology.
Step 1: walk the happy path through Burp
The happy path is the normal way a customer uses the app. With Burp's browser open and Burp recording (Burp Suite Community: Proxy Setup), do these in order:
- Browse the product list and open one product's details.
- Search for a product.
- Register a new account with a made-up lab email such as
aarav@lab.test, then log in. - Add two products to the basket and open the basket page.
- Go through checkout with fake address and card details. The Juice Shop guide itself warns never to enter real personal or card data.
- Leave customer feedback.
After this you should see dozens of requests in Proxy > HTTP history, and a tree of paths in Target > Site map.
Step 2: read what the browser called
Look at the site map. Juice Shop is a single-page application (SPA): the browser loads one page, and JavaScript then calls JSON APIs. So the interesting map is the API, not the pages. Frontend pages look like #/login, #/register and #/basket, where the part after # is handled in the browser and never sent to the server.
From the happy path you will typically see endpoints like these (example from the course lab, checked against the Juice Shop source code):
| Endpoint | Method | What it does | Inputs |
|---|---|---|---|
/rest/products/search | GET | Product search | q parameter |
/rest/user/login | POST | Log in | JSON email, password |
/rest/user/whoami | GET | Current user details | Auth token |
/rest/basket/{id} | GET | Read a basket | Basket ID in the path |
/api/BasketItems | POST | Add a product to a basket | JSON ProductId, BasketId, quantity |
/api/Feedbacks | POST | Send feedback | JSON comment, rating, captcha answer |
Two inputs should make a tester sit up already. /rest/basket/{id} takes a basket number in the URL; what if you change it? /api/BasketItems takes a BasketId in the body; who checks that the basket is yours? You will test exactly these in Testing Broken Access Control.
Step 3: turn history into an inventory
Raw history is noisy: the same endpoint appears many times with different IDs. A small script can normalise paths, so /rest/basket/6 and /rest/basket/7 become one entry, and list the parameters seen:
import re
from urllib.parse import urlsplit, parse_qs
history = [
("GET", "http://192.168.56.101:3000/rest/products/search?q="),
("GET", "http://192.168.56.101:3000/rest/products/search?q=apple"),
("POST", "http://192.168.56.101:3000/rest/user/login"),
("GET", "http://192.168.56.101:3000/rest/basket/6"),
("GET", "http://192.168.56.101:3000/rest/basket/7"),
("POST", "http://192.168.56.101:3000/api/BasketItems/"),
("GET", "http://192.168.56.101:3000/api/Products/24?d=Mon"),
]
inventory = {}
for method, url in history:
parts = urlsplit(url)
path = re.sub(r"/\d+(?=/|$)", "/{id}", parts.path.rstrip("/"))
entry = inventory.setdefault((method, path), set())
entry.update(parse_qs(parts.query, keep_blank_values=True))
for (method, path), params in sorted(inventory.items(), key=lambda item: item[0][1]):
shown = ", ".join(sorted(params)) or "-"
print(f"{method:<5} {path:<24} params: {shown}")POST /api/BasketItems params: -
GET /api/Products/{id} params: d
GET /rest/basket/{id} params: -
GET /rest/products/search params: q
POST /rest/user/login params: -Seven history lines became five endpoints. POST bodies are not in the URL, so their JSON fields need a separate note. In Burp you can read them in each request.
Step 4: look for what is not linked
Applications often expose things no page links to. Check these in your lab:
curl -s http://192.168.56.101:3000/robots.txt
curl -s -o /dev/null -w "%{http_code}\n" http://192.168.56.101:3000/ftp
curl -s -o /dev/null -w "%{http_code}\n" http://192.168.56.101:3000/api-docs/User-agent: * Disallow: /ftp 200 200
robots.txttells search engines what not to index. It is public, so it often points straight at interesting places. Here it names/ftp./ftpin Juice Shop shows a directory listing of files. A real company would never want that public./api-docsserves Swagger API documentation for part of the API. Documentation is useful to attackers too.
Juice Shop also has a hidden score board page that tracks your progress. Nothing links to it. The project's guide hints that you can find it by looking behind the scenes, or by guessing from the pattern of other frontend paths such as #/login. Try it.
Step 5: read the JavaScript
An SPA ships its routes and API calls to your browser inside JavaScript files. In Burp's history, find the large main.js file and search it (Ctrl+F in the response view) for path: or /rest/. A script can pull route-like strings out of saved JavaScript text:
import re
js_sample = '''
{path:"login",component:Ln},{path:"basket",component:Bk},
{path:"administration",component:Ad,canActivate:[AdminGuard]},
this.http.get(this.host+"/rest/user/whoami"),
this.http.post(this.hostServer+"/rest/user/data-export",e)
'''
routes = re.findall(r'path:"([\w-]+)"', js_sample)
apis = re.findall(r'"(/(?:rest|api)/[\w/-]+)"', js_sample)
print("Frontend routes:", ", ".join(f"#/{r}" for r in routes))
print("API calls: ", ", ".join(apis))Frontend routes: #/login, #/basket, #/administration API calls: /rest/user/whoami, /rest/user/data-export
The sample is a made-up, simplified piece of a bundle, so the code is easy to read. Real bundles are minified, but the same searches work. An administration route protected by a guard in the browser is a lead: is the matching API also protected on the server?
Common mistakes
- Attacking before mapping. You miss most of the app and test the wrong things.
- Ignoring the API. In single-page apps, the API is the real attack surface.
- Forgetting robots.txt and docs. Public files often point to unlinked areas.
- Trusting browser-side guards. A route hidden in JavaScript says nothing about server checks.
Key points
- Use the app as a normal user through Burp before testing anything.
- In single-page apps, map the JSON API endpoints and their inputs.
- Normalise IDs in paths to build a clean endpoint inventory.
- Check
robots.txt, directory listings, API docs and JavaScript for unlinked features. - Every input you list becomes a test case in the following lessons.
Exercise
Extend the inventory script so it also counts how many times each normalised endpoint appeared, and marks endpoints with an ID in the path as access-control test needed.
Show solution
import re
from collections import Counter
from urllib.parse import urlsplit
urls = ["/rest/basket/6", "/rest/basket/7", "/rest/products/search?q=", "/api/Products/24", "/rest/basket/6"]
counts = Counter(re.sub(r"/\d+(?=/|$)", "/{id}", urlsplit(u).path) for u in urls)
for path, n in counts.most_common():
note = "access-control test needed" if "{id}" in path else ""
print(f"{path:<22} seen {n}x {note}")/rest/basket/{id} is seen 3 times and flagged, as is /api/Products/{id}. Any endpoint that takes an object ID needs a check that the user may access that object.
Quick Quiz
1. Why should you walk the happy path before testing?
Understanding normal behaviour shows you where to test and what "abnormal" looks like.
2. In Juice Shop, what is the part after # in #/basket?
Single-page apps handle # routes in the browser; the server sees only API calls.
3. What does Juice Shop's robots.txt point to?
It disallows /ftp, which turns out to have a directory listing.
4. The JavaScript has an administration route with a guard. What should a tester conclude?
Browser-side guards can be bypassed; only server-side checks protect data.
Learn Ethical Hacking with a mentor
Free lessons get you started. Our Ethical Hacking & Cyber Security training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
