This is a deliberately vulnerable local app with more realistic endpoint shapes for exercising the extension against live traffic without setting up a full lab stack.
python3 src/test/vulnerable_lab/app.pyDefault listener:
http://127.0.0.1:8008
Primary attack-driven smoke suite:
./gradlew smokeTestPlaybooksThis suite starts the lab automatically and drives the real attack classes, payload expansion, registry wiring, shared executor path, and URL Validation playbook without Burp.
Black-box lab self-check:
python3 src/test/vulnerable_lab/run_smoke_tests.pyPrime the session first:
GET /login HTTP/1.1
Host: 127.0.0.1:8008That issues session=lab-user, which keeps the requests authenticated enough for the bypass cases to be meaningful.
The lab is meant to double as a consultant demo environment. Each bypass-tab playbook has a named route with a plausible backend mistake behind it instead of everything collapsing onto /admin.
| Attack | Representative request | Expected bypass marker |
|---|---|---|
| Header | GET /edge/private/reports/quarterly with X-Forwarded-For: 127.0.0.1 |
trusted X-Forwarded-For |
| Path | GET /api/v1/../v1/reports/export |
path normalization bypass |
| Verb | OPTIONS /rest/admin/users/42 |
method confusion |
| Param | GET /api/internal/runtime/config?debug=true |
truthy query parameter |
| Cookie | GET /portal/account/export with Cookie: session=lab-user; debug=true |
truthy cookie parameter |
| Trailing Dot | GET /edge/admin/console with Host: 127.0.0.1. |
trusted trailing-dot Host |
| Trailing Slash | GET /api/v1/reports/export/ |
path normalization bypass |
| Extension | GET /api/v1/reports/export.json |
path normalization bypass |
| Content-Type | POST /graphql/internal/preferences with JSON body |
content-type parser confusion |
| Encoding | GET /api/v1/reports/%65xport |
path normalization bypass |
| Protocol | GET /legacy/admin/audit over HTTP/1.0 |
http-1.0 |
| Bearer Demo | GET /api/v2/admin/audit with Authorization: Bearer |
weak bearer token validation |
| Case | GET /API/V1/REPORTS/EXPORT |
path normalization bypass |
| URL Validation (Absolute URL) | GET /redirect/next?next={INJECT} |
url-allowlist-bypass |
| URL Validation (Hostname) | GET /host/check?host={INJECT} |
hostname-allowlist-bypass |
| URL Validation (CORS Origin) | GET /cors/profile with Origin: {INJECT} |
cors-origin-bypass |
| Realistic: Sacrificial-Prefix (unauth-capable) | GET /static/../admin/api/users |
sacrificial-prefix-exemption |
| Realistic: Per-Char Encoding | GET /api/profile/%61lice |
per-char-encoding |
| Realistic: Matrix-Suffix | GET /api/tenants/acme/invoices/INV-42;jsessionid=x |
matrix-param-suffix |
| Realistic: Double-Encoding | GET /api/gateway/%2561dmin |
double-encoding |
| Realistic: Splitter (unauth-capable) | GET /api/a%09udit/export |
splitter-via-stripped-chars |
| Realistic: Rewrite-Header (unauth-only) | GET /public/health + X-Original-URL: /internal/dashboard |
rewrite-header-unauth |
Base request:
GET /edge/private/reports/quarterly HTTP/1.1
Host: 127.0.0.1:8008
Cookie: session=lab-userExpected baseline:
403 edge report blocked
Expected bypasses:
- trusted reverse-proxy headers such as
X-Forwarded-For,X-Custom-IP-Authorization,X-Forwarded-Host - trusted rewrite headers such as
X-Original-URLandX-Rewrite-URL - useful for demos where an edge route is locked down but the upstream service trusts proxy metadata too much
Base request:
GET /api/v1/reports/export HTTP/1.1
Host: 127.0.0.1:8008
Cookie: session=lab-userExpected baseline:
403 reports export blocked
Expected bypasses:
- path normalization: case flips, encoded path segments, traversal-like normalizations, trailing slash, extension suffixes
- the same vulnerable path logic is also exposed on
/tenant/acme/billing/invoicesand/console/settings/users
Bearer-token example:
GET /api/v2/admin/audit HTTP/1.1
Host: 127.0.0.1:8008
Authorization: Bearer user-tokenExpected baseline:
403 bearer token lacks required scope
Expected bypass:
- weak bearer-token validation when the
Authorizationheader is replaced with values likeBearer,Bearer A, orBearer a a
Base request:
GET /rest/admin/users/42 HTTP/1.1
Host: 127.0.0.1:8008
Cookie: session=lab-userExpected baseline:
403 user-management blocked
Expected bypasses:
OPTIONS,HEAD, orTRACEreturn200because the route is protected by a naïve method allowlist- method override headers such as
X-HTTP-Method-Override: GETalso bypass
Base request:
GET /api/internal/runtime/config?debug=false&role=user HTTP/1.1
Host: 127.0.0.1:8008
Cookie: session=lab-userExpected baseline:
403 runtime config blocked
Expected bypasses:
- truthy query params such as
debug=true,is_admin=1,role=admin
Base request:
GET /portal/account/export HTTP/1.1
Host: 127.0.0.1:8008
Cookie: session=lab-user; debug=falseExpected baseline:
403 account export blocked
Expected bypasses:
- truthy cookies such as
debug=true,role=admin
Base request:
GET /graphql/internal/preferences?debug=false HTTP/1.1
Host: 127.0.0.1:8008
Cookie: session=lab-userExpected baseline:
403 graphql preferences blocked
Expected bypasses:
- if the request is converted to
POSTwithapplication/json,application/xml,text/xml, ormultipart/form-dataand the body still carriesdebug,is_admin,access, orrole, the app returns200 graphql preferences bypass granted via: content-type parser confusion
That makes it useful for testing the extension's content-type conversion behavior without also needing a value change.
Base request:
GET /edge/admin/console HTTP/1.1
Host: 127.0.0.1:8008
Cookie: session=lab-userExpected baseline:
403 edge console blocked
Expected bypass:
Host: 127.0.0.1.returns200 edge console bypass granted via: trusted trailing-dot Host
Base request:
GET /legacy/admin/audit HTTP/1.1
Host: 127.0.0.1:8008
Cookie: session=lab-userExpected baseline:
403 protocol blocked for HTTP/1.1
Expected bypass:
HTTP/1.0returns200 protocol bypass granted via HTTP/1.0
Base requests:
GET /redirect/next?next={INJECT} HTTP/1.1
Host: 127.0.0.1:8008
Cookie: session=lab-userGET /host/check?host={INJECT} HTTP/1.1
Host: 127.0.0.1:8008
Cookie: session=lab-userGET /cors/profile HTTP/1.1
Host: 127.0.0.1:8008
Origin: {INJECT}
Cookie: session=lab-userExpected baseline:
403on all three routes
Expected bypasses:
- absolute URL allowlist confusion on
/redirect/next - hostname allowlist confusion on
/host/check - weak CORS origin allowlist matching on
/cors/profile
The attack-driven smoke suite uses the real UrlValidationAttack with:
allowedHost=trusted.exampleattackerHost=127.0.0.1- explicit
{INJECT}markers in the request - raw payload mode for deterministic smoke coverage
The Burp UI for this tab is marker-driven:
- edit the request directly in the
URL Validationworkbench - place
{INJECT}where the payload should go Origin: {INJECT}should usually be paired with theCORSpayload family- choose the payload families you want to run:
Absolute URL,Host header, and/orCORS - default family selection is
Absolute URL - choose the attack settings you want to include:
Domain allow list bypass,Fake relative URLs,Loopback,IPv6,Cloud metadata endpoints,URL-splitting Unicode characters - choose one encoding mode:
Raw,Intruder's,Everything,Special chars, orUnicode escape View Payloadsshows the exact generated list before execution- results label the generated
Family,Encoding, and final injectedPayload
Note:
- this lab is built on Python stdlib
http.server, so it is useful forHTTP/1.0vsHTTP/1.1smoke checks - it is not a full HTTP/2 target
Five routes model specific WAF+origin disagreement bugs you'd find in production. Each baseline returns 403 with a realistic error JSON. Each bypass returns 200 with plausible sensitive data and an X-Smoke-Bypass header naming the technique. The handlers are implemented as two explicit layers (WAF check, then origin dispatch) so the bug shape is readable directly from the source.
Bug modeled: AuthZ middleware exempts asset-serving prefixes (/static/*, /public/*, /assets/*, /images/*) from the auth check. The exemption test runs on the RAW path; dispatch runs on the NORMALIZED path.
GET /admin/api/users HTTP/1.1 # baseline -> 403 admin role required
GET /static/../admin/api/users HTTP/1.1 # bypass -> 200, returns admin users JSONResponse on bypass: list of admin/service accounts (username, email, role, last_login).
Bug modeled: WAF matches the raw path segment literally against a block-list of high-value user profiles. The origin decodes %XX before dispatching.
GET /api/profile/alice HTTP/1.1 # baseline -> 403 PII block rule
GET /api/profile/%61lice HTTP/1.1 # bypass -> 200, returns alice's profileResponse on bypass: full user profile (id, email, MFA status, created_at).
Bug modeled: Tomcat/Jetty/Spring strip ;matrix=params from each segment before routing. A WAF regex anchored to the literal path won't match a URL with a matrix suffix, but the servlet container still dispatches to the invoice handler.
GET /api/tenants/acme/invoices/INV-42 HTTP/1.1 # baseline -> 403
GET /api/tenants/acme/invoices/INV-42;jsessionid=x HTTP/1.1 # bypass -> 200Response on bypass: invoice JSON (amount, line items, status, due date).
Bug modeled: The WAF URL-decodes once before its ACL check; the origin decodes twice (proxy + framework). A double-encoded payload passes the WAF's single-decoded view but still resolves on the origin.
GET /api/gateway/admin HTTP/1.1 # baseline -> 403 perimeter WAF
GET /api/gateway/%2561dmin HTTP/1.1 # bypass -> 200, returns gateway configResponse on bypass: gateway routing config, rate-limit settings, shared secret.
Bug modeled: The WAF decodes once and checks for literal substrings. The origin decodes once, then strips tab/LF/CR bytes before routing. A %09-tab embedded inside the protected segment decodes to a tab-containing string the WAF doesn't match, but the origin strips the tab and dispatches.
GET /api/audit/export HTTP/1.1 # baseline -> 403 deny-audit-egress
GET /api/a%09udit/export HTTP/1.1 # bypass -> 200, returns audit eventsResponse on bypass: recent audit events (actor, action, target, source IP, timestamp).
Three of the realistic routes support complete unauth bypass — no session cookie at all. These model real-world patterns where the authentication check was structural (middleware order, WAF path regex, edge vs origin trust) and the bypass technique routes around the check entirely. If an attacker hits these, they read sensitive data without any credentials.
Real-world pattern: Express/Nuxt/Nginx register static-file middleware BEFORE the auth middleware. The static middleware matches /static/*, /public/*, /assets/*, /images/* on the raw path and passes the request through untouched — including skipping the auth gate. The router then normalizes and dispatches.
GET /admin/api/users HTTP/1.1 # unauth baseline -> 401 login required
GET /static/../admin/api/users HTTP/1.1 # unauth bypass -> 200, returns user listNo Cookie: session=... header needed on the bypass.
Real-world pattern: the WAF applies BOTH its auth gate and its deny rule to paths matching a specific regex (/api/audit/*). A payload that embeds %09 (tab) inside the audit segment decodes to a tab-containing string the WAF's regex doesn't match — so neither the deny rule nor the auth gate runs. The origin strips the tab and dispatches normally.
GET /api/audit/export HTTP/1.1 # unauth baseline -> 401 login required
GET /api/a%09udit/export HTTP/1.1 # unauth bypass -> 200, returns audit eventsReal-world pattern: the edge gateway (IIS + ARR, Apache mod_rewrite, some Kong configs) enforces auth based on the request line. The application, however, reads X-Original-URL / X-Rewrite-URL / X-Forwarded-URI to decide the effective route. A request with a request line pointing to an unauth-exempt path plus a rewrite header pointing to the protected path bypasses the edge auth entirely.
GET /internal/dashboard HTTP/1.1 # unauth baseline -> 401
GET /public/health HTTP/1.1 # unauth bypass -> 200, returns dashboard
X-Original-URL: /internal/dashboardAlso works with X-Rewrite-URL and X-Forwarded-URI.
Successful bypass responses include:
- status
200 - header
X-Smoke-Bypass - body text naming the bypass reason
Blocked responses are 403, and unauthenticated requests are 401.
smokeTestPlaybooksis the main smoke suite. It uses the extension's real attack classes and runs until each playbook finds a representative bypass.run_smoke_tests.pyis retained as a fast black-box check for the lab itself, including the URL Validation routes.- Several playbooks intentionally converge on the same vulnerable behavior in this lab, especially path normalization around
/api/v1/reports/export,/tenant/acme/billing/invoices, and/console/settings/users. - The extra
/api/v2/admin/auditBearer-token route is there as a realistic consultant demo even though the mainHeaderplaybook smoke target is the reverse-proxy route. - The lab uses Python stdlib only, so it is easy to run locally and easy to inspect when a smoke case fails.