Why a dual-mode gateway instead of a pentest report
Most "I did a security assessment" project write-ups are a static list of findings and CVSS scores — believable, but unverifiable by the reader. This playground is built differently: a CyberMart target application (Express + SQLite, real bcrypt password hashing, JWT auth) sits behind a Smart Reverse Proxy & Hardening Gateway. The gateway has two runtime postures, toggled live from a Security Operations dashboard:
- VULNERABLE — the target's own unsanitized code paths are exposed directly: string-concatenated SQL, raw DOM injection, ambient-cookie auth with no anti-CSRF check, relative file paths with no traversal guard.
- HARDENED — the exact same request path now runs through WAF inspection at the gateway plus architectural fixes at the data sink: parameterized prepared statements, contextual HTML-entity encoding, canonical path confinement, double-submit CSRF tokens, and object-level authorization checks.
Because both modes run the identical application and the identical exploit script, there's no room for a cherry-picked demo. You send the SQL injection payload, watch it dump the users table in VULNERABLE mode, flip the gateway to HARDENED, send the same byte-for-byte request, and watch it come back 403 Forbidden with the specific defense that stopped it named in the response body.
The target: CyberMart and its attack surface
CyberMart is a small but realistic e-commerce app — products, search, reviews, orders, invoices, a login flow — deliberately built with the mistakes that show up in real audits rather than contrived textbook bugs. The baseline assessment found eight distinct OWASP Top 10 (2021) issues, summarized from the technical pentest report:
| Finding | OWASP | CVSS | Baseline | Hardened |
|---|---|---|---|---|
IDOR on /api/orders/:id | A01 | 8.5 High | 200 OK | 403 |
Weak JWT secret (secret123) | A02 | 7.5 High | 200 OK | 401 |
| UNION-based SQL injection | A03 | 9.8 Critical | 200 OK | 403 (WAF) / parameterized at sink |
| Stored XSS in reviews | A03 | 8.0 High | 201 OK | 403 (WAF) / encoded at sink |
| No login rate limiting | A04 | 5.3 Medium | 200 OK | 429 |
| Missing security headers | A05 | 6.5 Medium | Missing | Enforced (CSP, HSTS, X-Frame-Options…) |
CSRF on /api/user/email | A07 | 7.4 High | 200 OK | 403 |
| Path traversal on invoice download | A08 | 8.6 Critical | 200 OK | 400 |

Every one of these eight findings maps to a specific sink-level fix, not a generic "add input validation." The point of the hardening layer isn't to block bad-looking strings — it's to make the vulnerability class structurally impossible at the place the data actually gets used.
Case study: the drive-by localhost RCE
The strongest finding in this project wasn't in the target app at all — it was in the tooling built to demonstrate the target app. The SOC dashboard exposed an endpoint, /api/exploit/run, that let a researcher trigger an exploit script from the UI. Two decisions made it dangerous together: the dashboard ran cors() with no origin restriction, and the exploit name was interpolated directly into a shell command.
Any website in your browser could pop calc.exe on your machine
Because the dashboard accepted requests from any origin, a completely unrelated site — evil-attacker.com, opened in an ordinary browser tab while the lab happened to be running locally — could fire a silent background fetch() to http://localhost:3000/api/exploit/run with a crafted exploitType field. The handler built its shell command as exec("python security-toolkit/exploits/exploit_" + exploitType + ".py --json"), so a payload like sqli"; calc.exe # broke out of the intended argument and executed arbitrary commands with the local user's own permissions. No authentication, no user interaction beyond visiting a webpage — a classic drive-by RCE, sitting inside a tool meant to teach web security.
The remediation is a textbook demonstration of "don't sanitize the string, remove the class of bug": permissive CORS was replaced with a strict loopback origin guard; the shell interpolation was replaced with execFile('python', [scriptPath, '--json']), which passes arguments as an immutable array and never touches a shell parser at all; and the exploit name itself is now checked against a frozen whitelist (ALLOWED_EXPLOITS) of eight pre-approved module names, so even a fully-controlled string can't reach an unintended path.
exec() · Fix: origin lockout + execFile with argument arrays + exploit-name whitelistA second, related lesson came from trying to stop SQL injection at the gateway with regex alone. The WAF rule matched patterns like or '1'='1', and adversarial fuzzing broke it in three progressive rounds: quote-style asymmetry (1' oR '1'='1) defeated backreference matching, zero-whitespace token boundaries (zz'or'1'like'1) defeated \s+ assumptions, and operator mutations (1'||'1'='1, 1'OR(1)LIKE(1)) slipped past keyword lists entirely. The conclusion documented in the case study is the one every WAF vendor eventually has to admit: regex-based filtering at the perimeter is a leaky heuristic layer, not a fix. The actual remediation moved the query to a native parameterized prepared statement (WHERE name LIKE ?), which makes injection mathematically impossible regardless of how the payload is mutated — the WAF stays on as an early-warning telemetry signal, but the real guarantee lives at the data sink.
Vulnerable vs. hardened — the same dashboard, two runs
The clearest way to see the gateway's dual-mode design is the SOC dashboard itself, captured mid-exploitation in both postures. Every request/response pair, HTTP status code, and mitigation label shown below came from actually running the exploit suite against the live gateway — not a mockup.


The attack chain view makes the same point at a higher level. attack_chain_demo.py links four of the individual findings into one continuous compromise: a stored XSS payload in a product review fires when victim "Alice" views the page, silently POSTing to hijack her account email because there's no anti-CSRF check; the hijacked session is then used to pull a confidential admin order via IDOR; the order's invoice reference is fed into the path-traversal endpoint to exfiltrate the server's master database credentials and JWT secret. In VULNERABLE mode all four stages succeed end to end. In HARDENED mode the chain is severed at every single link — WAF + output encoding stops stage one, the CSRF token check stops stage two, the object-authorization guard stops stage three, and canonical path confinement stops stage four.

Tooling and detection: ZAP automation and a real SIEM layer
Manual exploit scripts prove a point once; automated scanning and detection prove it holds up under continuous testing. The playground wires in OWASP ZAP two ways — a fast native baseline scanner and the official containerized ZAP daemon via Docker — plus documented Burp Suite Community Edition proxy/Repeater/Intruder workflows for manual verification. Both ZAP paths produce standard HTML and machine-readable XML reports so the same scan can be read by a human or ingested by a CI pipeline.

Blocking a known exploit isn't detection — it's prevention. The project treats those as separate problems and builds a small SIEM layer on top of the gateway's structured JSON telemetry (gateway/logs/security_events.jsonl). Four Sigma rules, written in vendor-agnostic YAML, cover SQLi UNION extraction, stored XSS, path traversal, and IDOR, each tagged with its MITRE ATT&CK technique. The interesting engineering problem is that a stateless signature check can't tell an innocent typo from the first probe of an automated scanner — both look like one isolated request. siem_alert_engine.py solves this with a 30-second sliding window keyed by source IP, tracking mutation velocity and payload structural diversity (quote styles, comment delimiters, targeted endpoints). When five or more distinct payload mutations hit multiple endpoints from one IP inside that window, isolated per-event noise is suppressed and a single composite SCANNING_CAMPAIGN_DETECTED alert fires, mapped to MITRE ATT&CK T1595.002 — the difference between "here's a log line" and "here's an incident."


Proving the fix instead of asserting it
A hardening claim is only as credible as the regression testing behind it. The suite includes an 18-test Jest unit/integration pass covering bcrypt password hashing, token isolation, and contextual escaping, plus a property-based mutation fuzzer that throws 2,040 structurally-varied payloads at the WAF — different quote styles, whitespace patterns, encodings, and operator combinations — to stress-test exactly the kind of bypass class that broke the first-draft regex WAF during the drive-by RCE audit. Every push also runs a 21-stage GitHub Actions pipeline that scans the hardened posture specifically: if a reintroduced regression lets any payload through in HARDENED mode, the build fails outright rather than silently passing with `|| true`.


Notable engineering decisions worth a second look
- Tiered JWT key isolation — an early hardened build blanket-verified every token against one enterprise secret, which locked out legitimate users signed with the app's legacy key. The fix splits verification by claimed privilege: standard sessions use standard keys, admin claims are checked against a separate high-entropy secret, so a dictionary-cracked forgery (signed with the leaked
secret123) is cryptographically rejected while real users keep working. - Encode at the sink, not at the gateway — an early XSS fix double-escaped review text on both write and read, corrupting legitimate apostrophes into
&garbage. The final design stores comments raw and applies context-aware HTML-entity encoding exactly once, at the presentation output sink, matching the OWASP canonical model. - Password storage migrated from plaintext string comparison to salted bcrypt (`bcrypt.compare`), removing plaintext credential exposure at rest entirely rather than adding a hashing step on top of the existing broken comparison logic.
- The CI gate actually gates — it gets tested by deliberately reintroducing a known SQLi regression and confirming the pipeline fails, rather than trusting that the scanner would have caught it.
Full technical writeups — the threat model and STRIDE data-flow diagrams, the complete before/after code diffs for all eight findings, the ZAP automation and Burp Suite guides, and a bug-bounty report template — are in the repository's docs/ and reports/ folders.