EthicsComputerCourses
Join training +91 90532 10052

Mapping Juice Shop

Ethical Hacking · Lesson 13 of 28 Intermediate 7 min read Updated

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:

  1. What pages does the app have?
  2. What API endpoints do those pages call?
  3. What inputs does each endpoint accept (parameters, JSON fields, headers, files)?
  4. 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:

  1. Browse the product list and open one product's details.
  2. Search for a product.
  3. Register a new account with a made-up lab email such as aarav@lab.test, then log in.
  4. Add two products to the basket and open the basket page.
  5. Go through checkout with fake address and card details. The Juice Shop guide itself warns never to enter real personal or card data.
  6. 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):

EndpointMethodWhat it doesInputs
/rest/products/searchGETProduct searchq parameter
/rest/user/loginPOSTLog inJSON email, password
/rest/user/whoamiGETCurrent user detailsAuth token
/rest/basket/{id}GETRead a basketBasket ID in the path
/api/BasketItemsPOSTAdd a product to a basketJSON ProductId, BasketId, quantity
/api/FeedbacksPOSTSend feedbackJSON 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:

PythonRun it yourself
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}")
Output
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:

Terminal
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/
Output
User-agent: *
Disallow: /ftp
200
200
  • robots.txt tells search engines what not to index. It is public, so it often points straight at interesting places. Here it names /ftp.
  • /ftp in Juice Shop shows a directory listing of files. A real company would never want that public.
  • /api-docs serves 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:

PythonRun it yourself
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))
Output
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
Python
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?

2. In Juice Shop, what is the part after # in #/basket?

3. What does Juice Shop's robots.txt point to?

4. The JavaScript has an administration route with a guard. What should a tester conclude?

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