08 · Web application security
Depth: developed lesson with a bundled lab. See the content and coverage map.
Outcomes
Model browser, server, session, database, file, and identity boundaries. Test input handling in a deliberately vulnerable local application and write a finding that separates impact from demonstration.
Model: identity is only the first decision
A request names both an actor and an object. Knowing who the actor is does not establish whether that actor may read the object. For example, Alice may read her own record while Bob's record remains private. The server must apply this rule for each requested object, even if the user interface hides other record IDs.
In this exercise, an intentionally simplified header names a fictional user. That header simulates the result of authentication; it is not a secure login. Keep the header constant while changing the record ID so the experiment isolates the object authorization decision.
Lab: start a reproducible target
This original synthetic exercise uses Python 3.12 or newer, curl, and Git.
It requires no private books, Python packages, Docker, models, or real accounts.
In a terminal, clone the course repository (or enter your existing checkout):
git clone https://github.com/cristianexer/Deadwire.git
cd Deadwire
python3 --version
git rev-parse HEAD
python3 -m labs.web_boundary --mode vulnerable
Keep this terminal open and record the commit ID with your evidence. The service
binds to 127.0.0.1:8080, contains two fictional records, and never reads or writes
student files. If port 8080 is occupied, stop and identify the existing service;
alternatively use --port 8081 and change the port in every request below.
Lab: request lifecycle
Open a second terminal. Predict the status of each request before sending it:
curl --include --max-time 5 http://127.0.0.1:8080/
curl --include --max-time 5 -H 'Accept: application/json' http://127.0.0.1:8080/api/health
Use the lab’s test account and record status, headers, body shape, and correlation ID. Build a matrix of authentication, authorization, validation, output encoding, and error behavior. Do not probe public targets or paste session cookies into Git.
The health response should identify mode: vulnerable and synthetic: true.
Every GET response has an X-Request-ID matching an entry in the server terminal.
For this JSON-only fixture, HTML output encoding and database input validation
are outside the exercise; record those cells as not evaluated.
Lab: change one variable
The fixture recognizes alice and bob as simulated identities. First establish
the unauthenticated baseline, then compare two objects using the same identity:
curl --include --max-time 5 http://127.0.0.1:8080/api/records/101
curl --include --max-time 5 -H 'X-Lab-User: alice' http://127.0.0.1:8080/api/records/101
curl --include --max-time 5 -H 'X-Lab-User: alice' http://127.0.0.1:8080/api/records/202
The first request returns 401. The next two return 200 in vulnerable mode, even though record 202 names Bob as its owner. This demonstrates one unauthorized read in a synthetic two-record system. It does not establish write access, account takeover, enumeration at scale, or the security of another application.
Lab: test the control and reset
Press Ctrl-C in the server terminal, then restart in fixed mode:
python3 -m labs.web_boundary --mode fixed
Repeat the three requests and add a positive control using Bob's identity:
curl --include --max-time 5 -H 'X-Lab-User: bob' http://127.0.0.1:8080/api/records/202
curl --include --max-time 5 -H 'X-Lab-User: alice' http://127.0.0.1:8080/api/records/999
| Simulated identity | Record | Vulnerable mode | Fixed mode | Interpretation |
|---|---|---|---|---|
| Absent | 101 | 401 | 401 | Missing identity is rejected |
| Alice | 101 | 200, owner Alice | 200, owner Alice | Own-record access remains available |
| Alice | 202 | 200, owner Bob | 403, no record | Owner authorization stops the cross-user read |
| Bob | 202 | 200, owner Bob | 200, owner Bob | The control does not deny every request |
| Alice | 999 | 404 | 404 | A missing record is distinct from denied access |
The implementation changes one rule: compare the record owner with the simulated identity before returning its contents. Review it in the fixture source. It still trusts a client-supplied mock identity and distinguishes missing from forbidden records. Neither mode is a production authentication design.
Stop the server with Ctrl-C when finished. Restarting it restores the same two records; there is no persistent database or cleanup directory. Confirm that a subsequent health request fails to connect, then record the reset result. If any response differs from the matrix, check the health mode and port before changing the experiment.
Write the finding
Save the prediction, commit ID, mode, simulated identity, record ID, response status, owner field (when present), and request ID for each case. Include the matching server log entry. Explain the unauthorized read, the ownership check, and why Alice's own read and Bob's own read must still pass. Label this as a synthetic lab result; no source claim or learner mastery is automatically approved.
Review questions
- Which component owns the security decision?
- Can a request cross an object boundary by changing an identifier?
- Does the evidence reproduce after a clean reset?
- What log would distinguish abuse from a normal client?
The source map includes web application books; their examples become reviewed lessons only after they pass the evidence contract.
Safety and scope
Use only an instructor-provided vulnerable application or a local fixture. Keep requests bounded, use test accounts, and never reuse session cookies or payloads against an external target.
Teach-back
Explain which component owns the security decision, what evidence demonstrates the boundary failure, and how a defensive control would be tested.
Evidence
Save request and response metadata, test-account scope, reset state, and the exact observation that supports the finding. Never retain session secrets.