Web Application Security · beginner · ~11 min
**What you will learn** - Configure an intercepting proxy (Burp Suite or OWASP ZAP) to sit between your browser and a **local, authorized** lab app. - Run the core web-testing loop: **intercept → modify → forward → observe → repeat** on requests you send yourself. - Change the parts of a request the UI hides or forbids — parameters, headers, cookies, and the HTTP method — and read how the server responds. - Reproduce the same request outside the browser with `curl`, so a finding is scriptable and repeatable. - Explain, from a defender's seat, **why** trusting client-controlled request data is the root cause of most web bugs, and what to log to detect tampering. - Follow an authorization checklist and lab cleanup so all practice stays legal and isolated.
Security objective. The asset is the web application's backend logic and data — prices, user records, order IDs, permissions. The threat is a client who sends the server a request it did not expect: a changed field, a forged header, someone else's ID. In this lesson you learn to detect whether the server trusts data it should not, by safely shaping your own requests through a proxy in a local lab, and you learn what the defending server should validate and log.
Web testing, stripped to its essence, is sending requests the application did not anticipate and watching how it responds. The user interface — buttons, dropdowns, forms — is only one client talking to the server. The server actually speaks HTTP, and HTTP is just text you can read and rewrite.
An intercepting proxy is the tool that makes this visible. It pauses each request between browser and server so you can read and edit it before it is sent. This is how you change a value a form marked read-only, swap an id, or flip a method the UI never offered.
This builds directly on your prerequisites. From How the web works: client, server, request, response, you already know a request travels from a client to a server and comes back as a response — the proxy simply parks itself on that road. From REST APIs, HTTP methods, and JSON, you know a request has a method (GET, POST, …), a path, headers, and often a JSON body — those are exactly the fields you will learn to reshape. Everything later in this track (broken access control, injection, and more) is a specific pattern of this one skill.
In authorized professional work — a scoped penetration test, a bug-bounty engagement, or your own QA before release — almost every finding starts the same way: the server trusted something the client controlled. The intercept-modify-forward loop is the universal way to demonstrate that trust and to prove whether a fix removes it.
A tester who can shape requests can show a developer the exact HTTP bytes that trigger a bug, not a vague description. A defender who understands the loop knows why input must be re-validated on the server and why hidden form fields, disabled buttons, and client-side checks protect nothing. The same skill drives the verification step: after a fix ships, you replay the malicious request and confirm the server now rejects it. Without this loop you are guessing; with it, findings and fixes become concrete and repeatable.
Definition. A local tool (Burp Suite, OWASP ZAP) that sits between your browser and the server, pausing each request so you can read and edit it before it is forwarded.
Plain explanation. Normally your browser sends a request straight to the server. With a proxy configured, the request stops at the proxy first. You see the raw HTTP text, change any part, and release it. The server has no idea the UI didn't produce those bytes — to it, HTTP is HTTP.
How it works. You point the browser's proxy setting at the tool (commonly 127.0.0.1:8080). For HTTPS the proxy presents its own CA certificate, which you install in the browser only in your test profile, so it can read encrypted traffic. Intercept mode holds requests; you edit and click Forward.
When / when not. Use it to inspect and tamper with your own traffic to an authorized target. Do not run it against sites you do not own or have written permission to test, and never leave its CA certificate trusted in your everyday browser profile.
Pitfall. Forgetting the proxy is on: your normal browsing hangs or throws certificate errors. Use a separate browser profile for testing so you can turn the whole thing off cleanly.
Definition. Talking to the backend directly instead of through the web page's controls.
Plain explanation. The form may cap a quantity at 10, grey out a price field, or hide an admin option. None of that reaches the server as a rule — it is decoration. The server only receives the final request, and the proxy lets you write that request by hand.
How it works. Intercept the request the form would send, then set quantity=9999, restore a removed field, or change price. If the server accepts it, the real control was never there.
When / when not. This is the heart of testing access control and business-logic flaws in a lab. Out of scope, it is unauthorized tampering.
Pitfall. Concluding the app is broken from a changed request alone. A finding requires the server's response to show it acted on the tampered value.
Definition. The repeatable cycle: intercept a request, modify one thing, forward it, observe the response, then repeat.
Plain explanation. You change one variable at a time so you can attribute the effect. Change too much at once and you cannot tell what mattered.
How it works. Capture a baseline (unmodified) response first. Then alter a single field, forward, and compare status code, length, and body against the baseline. A meaningful difference is a signal.
When / when not. Always work from a baseline. Skipping it means you have nothing to compare against.
Pitfall. Changing method, a header, and a parameter together — now a difference could come from any of the three.
Definition. Supporting tools that make single requests, fuzz at scale, or inspect the live page.
curl — send one exact request from the command line; ideal for capturing a reproducible proof.ffuf / gobuster — fuzz (send many auto-generated inputs) to discover parameters or paths. Use only against authorized lab targets.Pitfall. Reaching for a noisy fuzzer before you understand a single request by hand — you drown a lab in traffic and learn little.
Definition. The rule that testing happens only on systems you own or are explicitly permitted to test, in an isolated environment.
Plain explanation. The technique is neutral; legality comes from scope. Practice targets must be intentionally vulnerable apps you run yourself — DVWA or OWASP Juice Shop — on localhost or in a container with no route to the public internet.
Pitfall. Assuming "it's just my browser" makes any target fair game. Authorization is about the target, not the tool.
THREAT MODEL — intercepting your own traffic to a LOCAL lab
[ You / test browser ] TRUST BOUNDARY [ Lab server ]
test profile only | DVWA / Juice Shop
| | on localhost
| (1) request | |
v | |
+---------------------+ (2) edited | (3) forwarded v
| Intercepting proxy | ---------------- | -----------------> Backend logic
| 127.0.0.1:8080 | | + data store
| (Burp / ZAP) | <--------------- | <----------------- (the ASSET)
+---------------------+ (4) response | server reply
^ |
| |
ENTRY POINTS the server sees: method, path, headers,
cookies, query string, request body -> ALL client-controlled
Everything left of the boundary is attacker-controllable input.
The server MUST re-validate it; client-side rules do not cross the line.
Isolation: lab has no route off localhost; proxy CA trusted in test profile only.
Knowledge check.
price field but the server still accepts a changed price from an intercepted request. What insecure assumption did the developer make?The unit you are shaping is a raw HTTP request. Knowing its structure tells you exactly which lines the proxy lets you edit.
POST /api/cart HTTP/1.1 # request line: METHOD PATH VERSION
Host: localhost:3000 # which virtual host / app
Cookie: session=<lab-session> # client-sent session token (editable text)
Content-Type: application/json
Content-Length: 27 # byte length of the body (below)
{"item":"book","price":40} # request body: JSON the server will parse
Every line above is client-controlled and therefore editable in an intercepted request: the method (POST→PUT), the path, any header, the Cookie, and each field in the body (price).
Proxy configuration, at a glance:
Browser (test profile) proxy -> HTTP/HTTPS host 127.0.0.1 port 8080
HTTPS interception -> install the proxy's CA cert in the TEST profile only
Intercept toggle -> ON = requests pause for editing; OFF = pass through
The same request as a scriptable curl command (note: change Content-Length automatically handled by curl):
curl -i -X POST http://localhost:3000/api/cart \
-H 'Content-Type: application/json' \
-b 'session=<lab-session>' \
-d '{"item":"book","price":40}'
Web testing is, at its heart, sending requests the application did not expect and watching how it responds.
Tools like Burp Suite and OWASP ZAP sit between your browser and the website. They let you pause, read, and edit every request before it is sent.
The workflow has four steps:
The frontend is just one way to talk to the backend. A proxy lets you skip the UI entirely.
With it, you can:
price field,id,Everything in this track is a pattern of doing exactly that.
Only test applications you own or are explicitly authorized to test.
The labs here are conceptual. To practice hands-on, use intentionally vulnerable apps like DVWA or OWASP Juice Shop, running locally.
This example uses a tiny lab API to show the full INSECURE → SECURE → VERIFY shape. The bug is a classic client-trusted field: the server bills whatever price the client sends.
WARNING: intentionally vulnerable — use only in a local, isolated, authorized lab. Do not deploy.
# insecure_app.py — Flask lab target on localhost ONLY
from flask import Flask, request, jsonify
app = Flask(__name__)
# Trusted server-side catalog (the source of truth the client cannot see)
CATALOG = {"book": 40, "pen": 2}
@app.post("/api/cart")
def add_to_cart():
data = request.get_json(force=True)
item = data.get("item")
price = data.get("price") # <-- INSECURE: price taken from the client
total = price # server bills whatever arrived
return jsonify(item=item, charged=total)
if __name__ == "__main__":
app.run(host="127.0.0.1", port=3000) # localhost only
Through the proxy you intercept the normal request and change "price":40 to "price":0. Forward it. The server replies {"item":"book","charged":0} — it trusted a client-controlled field. The UI never offered a price box; bypassing the UI made the flaw visible.
# secure_app.py — server owns the price; client 'price' is ignored
from flask import Flask, request, jsonify
import logging, uuid
app = Flask(__name__)
log = logging.getLogger("cart")
logging.basicConfig(level=logging.INFO)
CATALOG = {"book": 40, "pen": 2}
@app.post("/api/cart")
def add_to_cart():
corr = uuid.uuid4().hex[:8] # correlation id for the log
data = request.get_json(silent=True) or {}
item = data.get("item")
if item not in CATALOG: # allow-list validation
log.warning("cart reject corr=%s src=%s item=%r reason=unknown_item",
corr, request.remote_addr, item)
return jsonify(error="unknown item", corr=corr), 400
price = CATALOG[item] # <-- SECURE: server decides price
if "price" in data and data["price"] != price:
# client tried to override price -> log the tamper signal, ignore value
log.warning("cart price_mismatch corr=%s src=%s item=%s sent=%r server=%s",
corr, request.remote_addr, item, data.get("price"), price)
log.info("cart accept corr=%s src=%s item=%s charged=%s",
corr, request.remote_addr, item, price)
return jsonify(item=item, charged=price, corr=corr)
if __name__ == "__main__":
app.run(host="127.0.0.1", port=3000)
# Bad input: attacker-controlled price must be IGNORED
curl -s -X POST http://localhost:3000/api/cart \
-H 'Content-Type: application/json' \
-d '{"item":"book","price":0}'
# expected: {"charged":40,...} (NOT 0) -> fix holds
# Good input: normal request still works
curl -s -X POST http://localhost:3000/api/cart \
-H 'Content-Type: application/json' \
-d '{"item":"book"}'
# expected: {"charged":40,...}
# Unknown item: rejected with 400 and logged
curl -s -o /dev/null -w '%{http_code}\n' -X POST http://localhost:3000/api/cart \
-H 'Content-Type: application/json' \
-d '{"item":"laptop"}'
# expected: 400
Expected outcome. Against the insecure app the intercepted price:0 is charged as 0. Against the secure app the same tampered request is charged 40, the tamper attempt is logged with a correlation id, and unknown items return 400. The verification is the proof the fix removed the trust in client data — testing is not done until this replay passes.
Walking the secure handler, which is the part that matters for defence.
| Step | Line | What happens | Why it matters |
|---|---|---|---|
| 1 | corr = uuid.uuid4().hex[:8] |
Generate a short correlation id per request | Ties the accept/reject log lines for this request together for later tracing |
| 2 | data = request.get_json(silent=True) or {} |
Parse JSON, never crash on bad input | A malformed body becomes {} instead of a 500; robust to hostile input |
| 3 | item = data.get("item") |
Read the item name from the body | This is client-controlled; treated as untrusted |
| 4 | if item not in CATALOG: |
Allow-list check | Only known items pass; everything else is rejected, not sanitized-and-hoped |
| 5 | return ... 400 (+ log warning) |
Reject unknown item, log source and reason | The server, not the client, decides validity; the reject is auditable |
| 6 | price = CATALOG[item] |
Price comes from the server catalog | The heart of the fix: the client's price is never used to bill |
| 7 | if "price" in data and data["price"] != price: |
Detect a client trying to override price | A mismatch is a tamper signal, worth logging even though it is ignored |
| 8 | log.info("cart accept ...") |
Record the accepted decision | Security decision + result + source + correlation id, no secrets |
How values change during the attack request. The intercepted body is {"item":"book","price":0}.
price = data.get("price") → price = 0 → charged = 0. The client won.price = CATALOG["book"] → price = 40; the price:0 in the body only trips the mismatch log. charged = 40. The client's value never reached the total.That single line — where price is sourced — is the whole difference between vulnerable and safe.
1. Trusting client-side validation.
2. Calling a modified request a finding.
price to 0 and report a bug without reading the reply.charged:0) before claiming impact.3. Changing several things at once.
4. Leaving the proxy's CA certificate trusted everywhere.
5. Pointing tools at a target you were not authorized to test.
Browser hangs or shows a certificate error. The proxy is intercepting but its CA is not trusted in that profile, or intercept is stuck ON holding a request. Check: is the request paused in the proxy waiting for Forward? Is the CA installed in this profile?
No requests appear in the proxy. The browser is not actually proxied. Check the profile's proxy host/port matches the tool (127.0.0.1:8080), and that you are using the profile you configured, not a different browser.
Your edit seems to have no effect. Likely you edited the wrong copy, or the server ignored the field (which may itself be the secure behaviour). Ask: did I change the request that was forwarded, or a repeated one? Does the response body differ from the baseline at all?
HTTPS traffic is not captured. The CA certificate step was skipped or the app uses certificate pinning. For labs, run the target over plain http://localhost to keep interception simple.
Questions to ask when a test "fails":
curl so it is scriptable and shareable?Security & safety — detection and logging.
The defender's job is to notice request tampering and to keep an auditable trail without leaking secrets.
Log for every security-relevant request:
unknown_item, price_mismatch).Never log: passwords, session tokens or cookies, API keys, private keys, full payment card numbers (PANs), or PII you do not need. In the secure example the session is referenced, never printed, and the tampered price is logged as a value comparison — never a credential.
Events that signal abuse:
price_mismatch / rejected-field events from one source — someone is probing the trust boundary.PUT/DELETE where only GET/POST are used).How false positives arise: a legitimate client library may resend a field, a proxy or retry may duplicate a request, and a QA test may deliberately send odd input. Correlate volume and source, and whitelist known test agents, before treating a single mismatch as an attack. A tamper signal is a reason to look, not proof of malice.
Authorized use case. During a scoped web penetration test of an e-commerce app, a tester proxies their own session, intercepts the add-to-cart request, and finds the checkout total trusts a client price field. They capture the exact request as a curl command as evidence, report it, and — after the team moves pricing server-side — replay the request to verify the total is now correct. Same skill, both sides of the fix.
Professional best-practice habits (defender and tester):
| Beginner | Advanced | |
|---|---|---|
| Scope | One local lab app (DVWA / Juice Shop) on localhost | Written-scope engagement across many endpoints |
| Technique | Intercept and edit one field, read the reply | Chained requests, session handling, scripted curl/Repeater flows |
| Evidence | Screenshot + one curl reproduction |
Full request/response captures, correlation ids, retest matrix |
| Focus | Understand one request end to end | Coverage, minimizing noise, clean reporting and remediation retest |
All tasks are lab-only: run intentionally vulnerable apps (DVWA or OWASP Juice Shop) or the sample Flask app from this lesson on localhost / a container with no internet route.
Authorization checklist (before any task): (1) I own or run this target myself, or have written permission. (2) It is bound to 127.0.0.1 / an isolated container. (3) The proxy CA is installed only in my test browser profile. (4) I will reset the lab after (see cleanup).
Beginner 1 — Read a request.
Beginner 2 — Baseline then one change.
Intermediate 1 — Reproduce with curl.
curl command.curl with the right method, headers, cookie, and body; confirm it produces the same response.curl command and matching response.-X, -H, -b, -d; add -i to see status and headers.Intermediate 2 — Find a client-trusted field (defensive conclusion).
price). Confirm from the response that the server acted on it.Challenge — Detection and retest.
price_mismatch event (correlation id, source, sent vs server value, no secrets). Then replay both a tampered and a clean request and show: the tampered one is billed correctly and logged; the clean one is billed correctly and not flagged.Lab cleanup / reset: stop the local app and proxy; turn intercept OFF; reset the browser proxy setting; remove the proxy CA from the test profile when finished; restore DVWA/Juice Shop to its initial state (restart the container or use its reset option) so the next session starts clean.
127.0.0.1:8080 proxy config; the proxy CA in the test profile only; reproduce with curl -X … -H … -b … -d ….