Jeremy BryantCybersecurity portfolio

Hands-on security exercise / source reading

Burp practice: whose order is this?

Read the supplied instructions here, then download the complete project to use the Python utilities and training materials. The tool workflow requires the separate software and controlled environment described below. These are guided practice materials; reference checks do not assess independent understanding.

Download complete source ZIP

Archive folder: jeremy-bryant-pages/labs/burp-access-control/

Lab instructions

labs/burp-access-control/README.md

# Burp practice: whose order is this?

Use Burp Suite Community Edition to intercept a request, replay it manually,
change an order ID, and compare vulnerable and fixed server behaviour. The
original Lantern Orders application contains two fictional users and three
invented craft orders. The question is whether a user can read another user's
order even though the page only shows their own links.

Allow 45–75 minutes for the guided attempt and 20 minutes for a later repeat.
You use a terminal, a browser and Burp's manual tools. No programming is needed.

## Prerequisites and scope

- Python 3.10+ and Burp Suite Community Edition already available on the **same
  machine**. Use the bundled Burp browser. This exercise does not install tools,
  change a VM, configure your everyday browser, or require an account online.
- A terminal opened in this lab folder. On Windows, use PowerShell. Commands
  below use `python`; substitute your existing Python command if necessary.
- The only test target is this process at `http://127.0.0.1:8765/`. No external
  target, other local service, scanner, extension, Intruder job or paid feature
  is needed. The server has no host or target argument and always binds to
  `127.0.0.1`. Do not expose it through a tunnel or port forward.

Training simplification: selecting Ada Finch or Ben Reed creates a disposable
session cookie without a password. Anyone using this app can select either
account. This is **not real authentication**, a privacy service or an example
of production session design. Keep the same account cookie when comparing two
order IDs; changing the account as well would test a different question.

## Maintained command contract

`lab.py` is the single entry point. It uses only Python's standard library.

```text
python -B lab.py --help
python -B lab.py serve --help
python -B lab.py cases
python -B lab.py serve --mode vulnerable --scenario guided --out run/guided-vulnerable-01
```

`serve` runs until Ctrl+C. `--mode` must be explicit. `--scenario` is `guided`
or `changed`; the default is `guided`. `--port` defaults to 8765 and accepts
1024–65535. If that port is busy, stop this attempt and use a fresh output name
with `--port 8766`; use the newly printed URL and port throughout Burp.

`--out` is relative to this lab folder, regardless of the terminal's directory.
It must be a fresh direct child such as `run/attempt-01`. Absolute, nested,
traversing, existing, symbolic-link and Windows reparse/junction paths are
refused. An unsuccessful start may leave its new folder; use a new name rather
than overwriting it. Generated files stay under the ignored `run/` folder.
These checks are for accidental path mistakes, not hostile concurrent filesystem
changes by another local process. Exit codes: 0 normal stop or read-only command;
2 invalid arguments, output-path problem or unavailable port.

Each successful run creates:

| File | Purpose |
| --- | --- |
| `run.json` | Mode, scenario, exact URL and process ID; manual completion remains pending |
| `roster.json` | Synthetic owner roster used by this run |
| `worksheet.md` | Blank manual evidence table and explanation prompts |
| `events.jsonl` | Server outcomes with account labels and IDs, without session tokens |

The server serves only the home page, account selector and in-memory order
routes. It cannot browse or serve your folders. It does not read credentials,
fetch remote URLs, modify orders or send data elsewhere. Its loopback and
Host/Origin checks narrow this exercise's boundaries; they are not a security
claim about Python's development HTTP server.

## Guided attempt

1. Run the vulnerable command above. Leave that terminal open. Confirm its
   printed URL is `http://127.0.0.1:8765/`, mode is `vulnerable`, and scenario is
   `guided`. Open the generated `worksheet.md` in your usual text editor.
2. Start your existing Burp Community installation with a temporary project
   and default configuration. In **Proxy > Intercept**, leave interception off
   initially and use **Open Browser**. Burp's browser is already configured to
   proxy its traffic. In **Settings > Tools > Proxy > Proxy listeners**, confirm
   the listener used by that browser binds to loopback (normally
   `127.0.0.1:8080`), not all interfaces. The primary
   [Proxy settings reference](https://portswigger.net/burp/documentation/desktop/settings/tools/proxy)
   describes these listeners. The proxy port 8080 and application
   port 8765 have different jobs. Do not alter an unrelated listener.
3. In that browser, enter `http://127.0.0.1:8765/` exactly. Use HTTP, not HTTPS,
   and `127.0.0.1`, not `localhost`. No certificate installation is needed.
   Choose **Ada Finch**, then **Select account**. The home page should show Ada,
   **VULNERABLE**, `guided`, and links to orders 101 and 103.
4. In **Target > Site map**, right-click the `http://127.0.0.1:8765` node and
   choose **Add to scope**. If asked, exclude out-of-scope traffic from logging.
   Inspect the resulting scope entry in Settings and keep only this HTTP host
   and port for this temporary project. In **Proxy > HTTP history**, open the
   display filter and select **Show only in-scope items**. Scope filters help
   you focus; they do not block every possible out-of-scope request. Browse
   only this app in this browser. These controls follow the primary
   [scope guide](https://portswigger.net/burp/documentation/desktop/getting-started/setting-target-scope).
5. In **Proxy > Intercept**, turn interception on. In the browser, click Ada's
   order 101. The browser waits while Burp holds the request. Inspect
   `GET /api/orders/101`, the exact Host, and `Cookie: lab_session=...`. Leave
   this baseline unchanged. Click **Forward**, then turn interception off.
   Record the matching entry number from HTTP history. The JSON should show
   order 101 owned by `ada`. If unrelated traffic is held, drop it; do not send
   it to this lab. See the primary
   [interception guide](https://portswigger.net/burp/documentation/desktop/getting-started/intercepting-http-traffic).
6. In HTTP history, right-click that **GET order request** and choose **Send to
   Repeater**. In Repeater, confirm the destination is HTTP, `127.0.0.1`, port
   8765. Click **Send** for an unchanged baseline. Read the status, response
   body, `X-Lab-Mode` and `X-Lab-Scenario`. Write your prediction for order 102.
7. Change only `/api/orders/101` to `/api/orders/102` in that request. Keep its
   Host, method and Ada cookie. Click **Send** once. Record the response status
   and the returned owner and note. Compare with `roster.json`: guided order
   102 belongs to Ben. Explain what this proves about this endpoint. The
   [Repeater guide](https://portswigger.net/burp/documentation/desktop/getting-started/reissuing-http-requests)
   describes manual send, edit and response-history controls.
8. In the browser, go back to `/`, select Ben, and open order 102. Send this
   fresh GET to a **separate Repeater tab** labelled Ben. Verify his own 102,
   then change only its ID to 101. Record both results. Keep the Ada and Ben
   tabs separate; browser account changes do not rewrite an old captured
   request. Do not guess tokens or use real accounts.
9. Save screenshots or copied request/response evidence alongside the worksheet
   before closing this temporary Burp project. Label tokens by account in your
   notes. Do not treat the generated event log alone as proof that you used Burp.

## Apply the prepared fix and retest

No code edit is needed: fixed mode contains the server-side ownership check.
Stop the vulnerable process with **Ctrl+C in its terminal**, then start:

```text
python -B lab.py serve --mode fixed --scenario guided --out run/guided-fixed-01
```

Refresh `/` in Burp's browser. Confirm **FIXED** and `guided`. Select Ada again:
old session cookies become invalid after a server restart. Capture a fresh order
101 request and send it to a new Repeater tab. An old cookie returning 401 is
not evidence that the ownership fix works.

Predict each outcome, then manually perform the following checks with fresh
Ada and Ben sessions. Save the results in the fixed run's worksheet.

| Selected account | Request | Expected outcome in fixed mode |
| --- | --- | --- |
| Ada | `/api/orders/101` and `/api/orders/103` | 200; Ada's correct record is present |
| Ada | `/api/orders/102` | 403; no order note or record body |
| Ben | `/api/orders/102` | 200; Ben's correct record is present |
| Ben | `/api/orders/101` and `/api/orders/103` | 403; no order note or record body |
| Either valid account | `/api/orders/999` | 404; no such seeded order |
| Either valid account | `/api/orders/abc` | 400; invalid ID format |
| Cookie header removed | `/api/orders/101` | 401; select an account first |

The narrow rule is: **a selected training account may read an existing order
only when the server's owner field matches that account**. The browser link list
is the same in both modes. The fix matters at the requested object, not just at
the page that contains its link. A 200 must contain the correct record; a denied
response must exclude it. A status code alone is insufficient evidence.

## Stop, troubleshoot and hand over

Set interception off, save your evidence, and press Ctrl+C in the server
terminal. Wait for its stopped message, then reload the app URL: it should fail
to connect. Close the lab browser and temporary Burp project. No everyday
browser proxy settings or certificates were changed by these instructions.
The `run/` notes remain for later review. Preserve them; there is no automatic
deletion command. A fresh attempt uses a different run name.

If the browser waits, check for a held Proxy request and use Forward or turn
interception off. If history is empty, confirm you used **Open Browser** and
the exact printed URL, then check the scope filter. A 421 means the Host was not
the exact printed host and port. A 403 on `/session` means the browser's Origin
was missing or different; reload the form from the printed URL. A 401 after
restart means you need a new selected session. A bind error usually means the
port is already in use; do not kill an unknown service. Use the alternate port
and a new run folder. If Burp is unavailable, stop here: direct HTTP tests do
not replace the requested Burp practice.

Deliver a short finding, the before/after evidence table, proof of retained
own-record access, and a limitation. Use the worksheet's handover sentence.
Continue with [the drills](DRILLS.md). Open [the worked example](WORKED-EXAMPLE.md)
after writing your initial predictions.

## Software validation and limits

```text
python -B test_lab.py
```

The suite opens temporary **loopback** listeners and sends real HTTP requests.
It checks both modes, both accounts, both owner rosters, valid and invalid
requests, session restart behaviour, Host/Origin restrictions, and output-path
refusals. It also starts the actual CLI, checks its files and home page, stops
that test process, and confirms its socket closes. Scratch artifacts stay in
fresh `run/software-tests-*` and `run/cli-smoke-*` children.
An unsupported symlink-creation facility is reported as a skip, not a pass.
Passing tests establish behaviour of the exercise software. They do not establish
that a learner intercepted anything, understood the result, or completed a
manual Burp retest. **Hands-on completion remains pending until that evidence
and explanation are reviewed.**

This app models one read endpoint and an owner-only rule. It has no real
authentication, password handling, persistence, write operations, sharing,
roles, account recovery or production session protection. Its different 403
and 404 responses reveal whether an ID exists; this is deliberate for teaching
and does not demonstrate privacy protection. Success here is not a claim that
an entire application is secure.

Primary documentation checked 23 October 2026: the linked interception, scope
and Repeater guides, and PortSwigger's
[horizontal access-control workflow](https://portswigger.net/burp/documentation/desktop/testing-workflow/vulnerabilities/access-controls/horizontal-access-controls).
The external guides describe their own exercises; this lab uses only the local
Lantern Orders target. UI labels may vary with the installed Burp version.

Vocabulary: **proxy** observes traffic between a browser and server;
**intercept** pauses a request before forwarding; **Repeater** resends an edited
request manually; **object ID** selects a record; **session** identifies the
selected account in this toy app; **authorization** decides whether that account
may access that record; **horizontal access** crosses between accounts with the
same kind of privileges; **regression check** confirms necessary behaviour
continues after a change.

Worked example

labs/burp-access-control/WORKED-EXAMPLE.md

# Worked example: one changed ID

This is the expected reasoning for the guided scenario. It is not a record of
a learner's completed session. Keep it closed until you have written your first
prediction in the generated worksheet.

The seeded roster says Ada owns 101 and 103; Ben owns 102. A legitimate task is
for each person to read their own order. Hiding Ben's link from Ada is useful
presentation, but a user can edit a request independently of the page.

An intercepted baseline will resemble this. The cookie shown is a placeholder;
keep the real disposable token captured in your own current run.

```http
GET /api/orders/101 HTTP/1.1
Host: 127.0.0.1:8765
Cookie: lab_session=<Ada token captured in this run>

```

After sending the unchanged baseline through Repeater, the response is 200 and
its JSON includes `"id": "101"`, `"owner": "ada"`, and the fictional lantern
note. Record `X-Lab-Mode: vulnerable` and `X-Lab-Scenario: guided` as context.

Change only the request path to `/api/orders/102`. The selected Ada cookie stays
the same. In vulnerable mode the response is also 200, but the body says
`"owner": "ben"` and contains `Fictional order: 1 silver cardboard moon.`
The owner roster, unchanged account, requested ID and returned content together
support a finding: **the order-read endpoint returns another account's record
to Ada in the guided vulnerable configuration**. The status alone would not
establish that the wrong content was disclosed.

Reverse the direction with a freshly captured Ben request. His own 102 returns
200; changing only its ID to 101 returns Ada's record in vulnerable mode. These
two comparisons test the same failure in both directions.

After stopping the server and starting fixed mode, select each account again
and capture new requests. The same-ID comparisons now have these outcomes:

| Current account | ID | Vulnerable | Fixed | Meaning |
| --- | --- | --- | --- | --- |
| Ada | 101 | 200, Ada record | 200, Ada record | Necessary access retained |
| Ada | 102 | 200, Ben record | 403, error only | Cross-user read denied |
| Ada | 103 | 200, Ada record | 200, Ada record | A second legitimate record remains usable |
| Ben | 102 | 200, Ben record | 200, Ben record | Ben's necessary access retained |
| Ben | 101 / 103 | 200, Ada record | 403, error only | Reverse direction denied |

The server checks the selected account against the stored order owner before
returning a record in fixed mode. If every request returned 403, the change
would break legitimate use and would not satisfy this exercise's rule.

With a valid account, ID 999 returns 404 because no record was seeded. `abc`
returns 400 because it is not a supported three-digit ID. Removing the session
cookie returns 401. Those outcomes describe different conditions: none is a
substitute for testing a known other user's existing object with a valid
session. In particular, a stale token returning 401 after restart does not prove
the ownership check worked.

A bounded handover might read:

> In the local guided configuration, I kept Ada's session unchanged and changed
> order 101 to 102 in Repeater. The vulnerable response returned Ben's fictional
> note. After restarting in fixed mode and selecting fresh sessions, cross-user
> requests returned 403 without record content. Both accounts still received
> their own records. This supports the owner check for the tested read endpoint;
> it does not assess real authentication, writes or other application routes.

Use that wording only after doing and documenting those actions yourself.
Replace it with your actual observations and any mismatch. Keep the request and
response evidence private in the run folder and ask a reviewer to challenge
your explanation, especially the difference between 401, 403 and 404.

Practice drills

labs/burp-access-control/DRILLS.md

# Practice sequence

Use only this lab's printed loopback URL. Every attempt gets a new `run/` child.
Keep the worksheet and request/response evidence; no script records mastery.
No programming is required at any stage.

## 1. Guided: follow and explain

Follow the README with `guided` mode data. Pause before changing an ID and say
what will remain the same, what will change, and why the expected owner matters.
Complete the vulnerable and fixed comparisons. Then read the worked example
and annotate differences between your expectation and your actual evidence.

Acceptance: one intercepted baseline, manual Repeater comparisons for both
accounts, fixed-mode denial, preserved own access, and a bounded explanation.
An automated test result does not satisfy this acceptance check.

## 2. Prompted: use a small checklist

Close the worked example and start fresh runs, for example:

```text
python -B lab.py serve --mode vulnerable --scenario guided --out run/prompted-vulnerable-01
```

After your comparisons, stop it and start:

```text
python -B lab.py serve --mode fixed --scenario guided --out run/prompted-fixed-01
```

Use only these prompts: exact scope; selected identity; unchanged baseline;
one changed object ID; status plus content; fresh sessions after restart;
other-user denial; own-user access; one limitation. If you need the full guide,
mark where and continue. Needing a reminder is useful evidence for the next
practice session.

## 3. Independent: write the plan first

Using new names such as `run/independent-vulnerable-01` and
`run/independent-fixed-01`, reproduce the finding and retest without the worked
example. Before sending anything, write your smallest useful request matrix.
Include both accounts, each existing order, one unknown ID, one invalid ID and
one missing-session request. Predict statuses and response contents separately.

Produce a short handover from your evidence. Ask another person to check whether
the account actually stayed unchanged within each ID comparison. Only then
compare with the reference reasoning. Record any correction in the worksheet.

## 4. Changed scenario: ownership is reassigned

The IDs and titles remain familiar, but the owner assignments change. Ben now
owns 101 and 103; Ada owns 102. A memorised rule such as “102 must be denied”
will fail. The correct decision depends on both the selected account and the
current object's owner. ID 999 is still absent.

```text
python -B lab.py serve --mode vulnerable --scenario changed --out run/changed-vulnerable-01
```

Inspect that run's `roster.json`; build a new expected matrix. Intercept an
unchanged request for Ben's 103, keep his session, and compare his access to
102. Then repeat with Ada requesting her own 102 and Ben's 101. Stop the server:

```text
python -B lab.py serve --mode fixed --scenario changed --out run/changed-fixed-01
```

Select fresh sessions and retest the whole matrix. In fixed mode, Ben's 101 and
103 must remain available while his request for 102 is denied; Ada must retain
102 while 101 and 103 are denied. Do not relabel a 401 from an old cookie as a
successful access-control fix. Explain why the scenario changed your expected
answers without changing the underlying rule.

## 5. Teach it back

In three minutes, explain to a beginner:

- What the proxy, request path, session and stored owner each contribute.
- Why editing only an ID is a controlled comparison, and why selecting Ben in
  the account picker is a separate deliberate operation.
- Why hidden links do not enforce authorization.
- Why a denial and a successful legitimate request are both needed after a fix.
- Why 401 from a stale session, 404 for an absent object, and 403 for a known
  other user's object support different conclusions.
- What remains untested: real authentication, writes, sharing, roles and a
  production deployment.

Have the listener ask one unexpected question, for example: “What if you changed
the cookie and the ID together?” Save the question and your corrected answer.

## 6. Later recall

After at least a day, close these files and explain the owner rule from memory.
Sketch four checks: own access before, cross-user access before, own access
after, cross-user access after. Add a missing-session control. Explain what
would make a claimed successful retest inconclusive. Then compare with your
saved evidence and correct your explanation.

Optional next session: use changed data without this guide. A later independent
attempt, its unfamiliar case and an accurate explanation are stronger evidence
than repeating the same sequence immediately. Record practice honestly as
guided, prompted, independent or needing review; do not infer competence from
elapsed time, a status code or passing software tests.

Browse all 30 lab source pages