Web Application Security · beginner · ~11 min

Testing the web: intercepting and shaping HTTP requests

**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.

Overview

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.

Why it matters

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.

Core concepts

1. The intercepting proxy

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.

2. Bypassing the UI

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.

3. The intercept–modify–forward loop

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.

4. Companion tools

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.
  • Postman — build and organize API requests interactively.
  • Browser devtools — inspect the DOM, storage, and the Network tab of live traffic.
  • ffuf / gobusterfuzz (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.

5. Authorization and isolation

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.

  1. In the diagram, what is the protected asset, and on which side of the trust boundary does it sit?
  2. A form disables the price field but the server still accepts a changed price from an intercepted request. What insecure assumption did the developer make?
  3. Why must this loop be practiced only against a target you own or are authorized to test, even though you are only editing your own browser's requests?

Syntax notes

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 (POSTPUT), 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}'

Lesson

Web testing is, at its heart, sending requests the application did not expect and watching how it responds.

The intercepting proxy

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:

  1. Proxy your browser through the tool.
  2. Intercept a request, then change a parameter, header, cookie, or method.
  3. Forward the request and read the response.
  4. Repeat systematically across every input.

Why it is the foundation

The frontend is just one way to talk to the backend. A proxy lets you skip the UI entirely.

With it, you can:

  • submit values the form forbade,
  • change a hidden price field,
  • swap an id,
  • or alter the HTTP method.

Everything in this track is a pattern of doing exactly that.

Companion tools

  • curl -- scriptable single requests.
  • Postman -- building and testing API requests.
  • Browser devtools -- inspect the live DOM, storage, and network traffic.
  • ffuf / gobuster -- fuzz parameters and paths at scale. (To fuzz means to send many automatically generated inputs to find ones that break or reveal something.)

Legality

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.

Code examples

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.

1) Insecure server

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.

2) Secure server (the fix)

# 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)

3) Verify the fix (rejects bad input, accepts good input)

# 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.

Line by line

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}.

  • Insecure app: price = data.get("price")price = 0charged = 0. The client won.
  • Secure app: 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.

Common mistakes

1. Trusting client-side validation.

  • Wrong: The form limits quantity to 10 and hides the price, so the developer assumes those limits hold.
  • Why wrong: Form rules live in the browser; the proxy edits the request after the form. The server receives whatever bytes you send.
  • Corrected: Re-validate every field on the server against a server-owned source of truth (allow-lists, catalog prices, ownership checks).
  • Recognise/prevent: Ask "what stops a client from sending a different value?" If the only answer is UI, it is not a control.

2. Calling a modified request a finding.

  • Wrong: You change price to 0 and report a bug without reading the reply.
  • Why wrong: The proof is the server's behaviour, not your edit. The server may ignore the field.
  • Corrected: Confirm the response reflects the tampered value (e.g. charged:0) before claiming impact.
  • Recognise/prevent: No response evidence, no finding.

3. Changing several things at once.

  • Wrong: Flip the method, add a header, and edit a parameter in one shot, then see an error.
  • Why wrong: You cannot tell which change caused it.
  • Corrected: Baseline first, then change exactly one variable per iteration.
  • Recognise/prevent: Keep a note of the single delta for each forwarded request.

4. Leaving the proxy's CA certificate trusted everywhere.

  • Wrong: Installing the proxy CA in your daily browser and forgetting it.
  • Why wrong: Anything able to present that CA can silently read your real HTTPS traffic — a serious risk to you.
  • Corrected: Use a dedicated test browser profile; install the CA only there; remove it when done.
  • Recognise/prevent: Keep testing and everyday browsing in separate profiles.

5. Pointing tools at a target you were not authorized to test.

  • Wrong: "It's just curl / just my browser," aimed at a live third-party site.
  • Why wrong: Authorization is about the target, not the tool; this can be illegal.
  • Corrected: Run intentionally vulnerable apps you control on localhost, or work strictly within a written scope.
  • Recognise/prevent: Confirm scope in writing before the first request.

Debugging tips

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":

  1. What is my baseline response (status, length, body) with no changes?
  2. What single field did I change, and to what value?
  3. Does the response actually reflect that change, or did the server override it?
  4. Am I hitting the app I think I am (right host, right port, right endpoint)?
  5. Can I reproduce the exact request with curl so it is scriptable and shareable?

Memory safety

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:

  • Timestamp (with timezone).
  • Source identifier (IP, and authenticated user/session id reference, not the token itself).
  • Resource acted on (endpoint, item, record id).
  • Result (accepted / rejected) and the security decision (why, e.g. unknown_item, price_mismatch).
  • A correlation id so the accept/reject lines and downstream events for one request join up.

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:

  • Repeated price_mismatch / rejected-field events from one source — someone is probing the trust boundary.
  • A burst of unknown-parameter or unknown-path 400s (fuzzing).
  • Unexpected methods on an endpoint (PUT/DELETE where only GET/POST are used).
  • The same request replayed many times in seconds.

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.

Real-world uses

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):

  • Validate on the server against an allow-list or source of truth; never trust client-side rules.
  • Least privilege: the endpoint and its DB account do only what that action needs.
  • Secure defaults: reject unknown fields and methods rather than guessing intent.
  • Logging: record the security decision + correlation id; exclude secrets.
  • Error handling: fail closed with a clear status code (400/403), never leak stack traces.
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

Practice tasks

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.

  • Objective: Configure the proxy and capture one request without changing it.
  • Requirements: Proxy the test browser through Burp/ZAP; log in to the lab app; intercept one request and identify its method, path, headers, cookie, and body.
  • Output: A written breakdown of each part of that raw request.
  • Hints: Start with the login or add-to-cart request. Keep intercept ON only briefly.
  • Concepts: proxy setup, HTTP request structure.

Beginner 2 — Baseline then one change.

  • Objective: Practice the loop by changing exactly one field.
  • Requirements: Record a baseline response (status, length, body). Then intercept the same request, change one non-destructive field (e.g. a search term), forward, and compare.
  • Output: A two-row comparison of baseline vs modified response.
  • Constraints: Change one variable only.
  • Hints: Note the single delta each time.
  • Concepts: baseline comparison, intercept-modify-forward.

Intermediate 1 — Reproduce with curl.

  • Objective: Turn an intercepted request into a scriptable curl command.
  • Requirements: Take a POST request from the proxy and rebuild it as curl with the right method, headers, cookie, and body; confirm it produces the same response.
  • Input/Output: Input = the intercepted request; output = a curl command and matching response.
  • Hints: Use -X, -H, -b, -d; add -i to see status and headers.
  • Concepts: reproducibility, evidence capture.

Intermediate 2 — Find a client-trusted field (defensive conclusion).

  • Objective: Detect a field the server should not trust, then specify the fix.
  • Requirements: Using the lesson's insecure Flask app (or Juice Shop), intercept a request and alter a value the UI does not expose (e.g. price). Confirm from the response that the server acted on it.
  • Constraints: Lab only; read the reply before concluding.
  • Deliverable: Describe the insecure assumption, the server-side fix (source the value server-side), and a verification command that proves the tampered value is now rejected.
  • Concepts: bypassing the UI, response-based evidence, remediation + retest.

Challenge — Detection and retest.

  • Objective: Add tamper detection to a fixed endpoint and prove it works.
  • Requirements: Extend the secure Flask handler to log a 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.
  • Constraints: Never log tokens/cookies/PII. Lab only.
  • Deliverable: The log lines from both replays plus a one-line note on how a false positive could occur.
  • Concepts: detection/logging, mitigation verification, false positives.

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.

Summary

  • Core skill: web testing is intercepting and reshaping your own requests through a proxy, then reading how the server responds — the intercept → modify → forward → observe → repeat loop.
  • Key structures/commands: an HTTP request (method, path, headers, cookie, body) is all editable text; 127.0.0.1:8080 proxy config; the proxy CA in the test profile only; reproduce with curl -X … -H … -b … -d ….
  • Root cause of most web bugs: the server trusted client-controlled data. The fix is always to source and validate on the server (allow-lists, server-owned values), then verify the tampered request is now rejected.
  • Common mistakes: trusting client-side validation, reporting a change without response evidence, altering many fields at once, leaving the proxy CA trusted everywhere, and testing unauthorized targets.
  • Defence to remember: log the security decision with a correlation id and source; never log passwords, tokens, cookies, keys, or full PANs; watch for repeated mismatches and unexpected methods as abuse signals.
  • Always: test only on systems you own or are authorized to test, keep labs isolated on localhost, and reset the lab and remove the CA when done.

Practice with these exercises