# Chapter 3 Labs: Programming Principles for Secure Systems

This lab pack provides the hands-on environment for Chapter 3. The central asset is a deliberately vulnerable Flask application that exercises SQL injection, OS command injection, JWT algorithm confusion, Pydantic type coercion, and error-handling flaws. Every lab except Lab 1 targets this container.

## Prerequisites

- Docker Engine 24+ and Docker Compose v2.20+
- Approx. 800 MB disk space for images
- Python 3.12+ on the host (Lab 1 only - the recon notebook runs locally)
- Internet access for the initial image pull

## Quick Start

```
docker compose up -d vuln-app
```

Verify the app is healthy:

```
curl http://localhost:5000/healthz
```

To start the tools container (interactive):

```
docker compose --profile tools run --rm tools bash
```

## Lab 1 - Build the Recon Utility (host setup - not containerised)

**Objective:** Build the Python recon helper from Section 3.2 incrementally, practise the `str`/`bytes` distinction, safe `subprocess` use, and graceful error handling.

**Setup:** This lab runs on the host; no container is required.

```
python3 -m venv .venv
source .venv/bin/activate
pip install requests dnspython click
```

**Walkthrough:**

1. Open `recon_notebook_seed.py` as your starting point (or create `03_python_recon.ipynb`).
2. Run the recon helper against a host you own or `localhost`:
   ```
   python recon_notebook_seed.py localhost
   ```
3. Test the DNS extension (requires `dnspython`):
   ```
   python recon_notebook_seed.py localhost --dns
   ```
4. Break it deliberately: pass a non-existent hostname and confirm the output is a contained error dict, not a raw traceback.
5. Extend: add the `--json` flag (already present in the seed) and pipe the output to `jq`.

**Expected output:**

```
{'host': 'localhost', 'ip': '127.0.0.1', 'error': 'HTTPSConnectionPool...'}
```

**Verify completion:** The tool returns a dict on both success and failure, never a bare exception traceback.

**Cleanup:** Deactivate and remove the venv:
```
deactivate && rm -rf .venv
```

---

## Lab 2 - Exploit, then Fix, a Vulnerable Login

**Objective:** Demonstrate SQL injection and OS command injection by hand, then with sqlmap; apply parameterised queries and `shell=False` to fix both.

**Setup:** The `vuln-app` service must be running:
```
docker compose up -d vuln-app
```

**Walkthrough:**

1. Confirm the SQL injection flaw by hand:
   ```
   curl -s -X POST http://localhost:5000/login \
     -d "user=' OR '1'='1&password=x"
   ```
   The response should be `{"role": "user", "status": "ok"}` - authentication bypassed.

2. Automate discovery with sqlmap (run inside the tools container):
   ```
   docker compose --profile tools run --rm tools bash -c \
     "sqlmap -u 'http://vuln-app:5000/login' --data='user=x&password=y' --batch --dbs"
   ```

3. Confirm the OS command injection in `/ping`:
   ```
   curl -s "http://localhost:5000/ping?host=127.0.0.1;id"
   ```
   The `id` output appears in `stdout` - the shell expanded the semicolon.

4. **Fix 1:** Compare `/login` (concatenation) with `/login-safe` (parameterised):
   ```
   curl -s -X POST http://localhost:5000/login-safe \
     -d "user=' OR '1'='1&password=x"
   ```
   Returns `401 {"status": "fail"}`. The same payload is now inert data.

5. **Fix 2:** Compare `/ping` (shell=True) with `/ping-safe` (argv list):
   ```
   curl -s "http://localhost:5000/ping-safe?host=127.0.0.1;id"
   ```
   The semicolon is passed as a literal argument to ping - no shell expansion.

6. Re-run sqlmap against `/login-safe` and confirm it finds nothing:
   ```
   docker compose --profile tools run --rm tools bash -c \
     "sqlmap -u 'http://vuln-app:5000/login-safe' --data='user=x&password=y' --batch --dbs"
   ```

**Expected output:** Step 1 returns HTTP 200 with a role; steps 4 and 5 return the expected rejections.

**Verify completion:** sqlmap returns "no injectable parameters" against `/login-safe`.

**Cleanup:** `docker compose down`

---

## Lab 3 - Drill: Break a Model, Then Make It Strict

**Objective:** Observe how Pydantic lax mode silently accepts object-type payloads (the NoSQL operator-injection vector), then enable strict mode to reject them.

**Setup:** `vuln-app` must be running.

**Walkthrough:**

1. Confirm the happy path against the strict endpoint:
   ```
   curl -s -X POST http://localhost:5000/order-strict \
     -H "Content-Type: application/json" \
     -d '{"quantity": 2, "status": "paid"}'
   ```

2. Send the NoSQL-style operator payload to the lax endpoint:
   ```
   curl -s -X POST http://localhost:5000/order-lax \
     -H "Content-Type: application/json" \
     -d '{"quantity": 2, "status": {"$ne": "x"}}'
   ```
   The lax endpoint accepts it. Note the coercion that passed the object as a status value.

3. Send the same payload to the strict endpoint:
   ```
   curl -s -X POST http://localhost:5000/order-strict \
     -H "Content-Type: application/json" \
     -d '{"quantity": 2, "status": {"$ne": "x"}}'
   ```
   Returns 422 with a ValidationError. Strict mode rejected the object because a `str` was expected.

4. Send a string quantity to both endpoints:
   ```
   curl -s -X POST http://localhost:5000/order-lax \
     -H "Content-Type: application/json" \
     -d '{"quantity": "5", "status": "paid"}'
   # Lax: accepted (coerced "5" to 5)

   curl -s -X POST http://localhost:5000/order-strict \
     -H "Content-Type: application/json" \
     -d '{"quantity": "5", "status": "paid"}'
   # Strict: rejected (str is not int)
   ```
   Decide which behaviour is correct for your field and record the decision in writing.

5. Confirm that the ValidationError from the strict endpoint is a generic client message plus a `ref` ID, not a detailed traceback (Section 3.8 behaviour).

**Expected output:** Step 3 returns HTTP 422; step 2 returns HTTP 200.

**Verify completion:** The strict endpoint rejects all object-typed payloads for `status` while accepting the valid `{"quantity": 2, "status": "paid"}`.

**Cleanup:** `docker compose down`

---

## Lab 4 - Forge, then Fail to Forge, a JWT

**Objective:** Demonstrate the `alg:none` and RS256->HS256 algorithm-confusion attacks against a naive JWT verifier, then apply the RFC 8725 fix (pinned algorithm) to defeat both.

**AUTHORISATION:** Run only against `http://localhost:5000`. Forging tokens against systems you do not own is unlawful; see Chapter 9.

**Setup:** `vuln-app` must be running.

**Walkthrough:**

1. Mint a legitimate RS256 token:
   ```
   curl -s "http://localhost:5000/jwt-issue?user=alice&role=user"
   ```

2. Run the lab helper script (inside the tools container for convenience):
   ```
   docker compose --profile tools run --rm tools \
     python /jwt_lab_helper.py
   ```
   The script walks all four steps: mint legitimate, forge `alg:none`, forge HS256-confusion, then show the safe endpoint rejecting both.

3. Or run the steps manually. Fetch the public key:
   ```
   curl -s http://localhost:5000/jwt-pubkey
   ```

4. Verify a legitimate token against both endpoints:
   ```
   TOKEN=$(curl -s "http://localhost:5000/jwt-issue?user=alice&role=user" | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")

   curl -s -X POST http://localhost:5000/jwt-verify-naive \
     -H "Content-Type: application/json" -d "{\"token\": \"$TOKEN\"}"

   curl -s -X POST http://localhost:5000/jwt-verify-safe \
     -H "Content-Type: application/json" -d "{\"token\": \"$TOKEN\"}"
   ```

5. After running the helper, write a two-sentence summary: which exact line changed, and why pinning `algorithms=["RS256"]` defeats both the `alg:none` and the HS256-confusion attack.

**Expected output:** The helper shows the naive endpoint accepting forged tokens; the safe endpoint rejecting them both with HTTP 401.

**Verify completion:** Both forged tokens return `{"status": "rejected", ...}` from `/jwt-verify-safe`.

**Cleanup:** `docker compose down`

---

## Lab 5 - Make a Vulnerable Endpoint Fail Safely

**Objective:** Reproduce CWE-209 (information leakage via raw exception) and CWE-636 (fail-open authorization), then rewrite both handlers to fail closed with a server-side log and a correlation ID.

**Setup:** `vuln-app` must be running.

**Walkthrough:**

1. Reproduce the information leak (CWE-209):
   ```
   curl -s "http://localhost:5000/search-leaky?q=' OR '1'='1"
   ```
   Read the error text returned. Note what reconnaissance the server volunteered: table names, file paths, query internals.

2. Trigger the safe version:
   ```
   curl -s "http://localhost:5000/search-safe?q=' OR '1'='1"
   ```
   Returns `{"error": "An error occurred", "ref": "..."}`. Check the container log for the full traceback:
   ```
   docker compose logs vuln-app
   ```
   The diagnostic detail is there, server-side only.

3. Reproduce the fail-open (CWE-636):
   ```
   curl -s "http://localhost:5000/admin-check-failopen?token=garbled"
   ```
   The token is invalid; observe what the response says about access.

4. Trigger the safe version with the same garbled token:
   ```
   curl -s "http://localhost:5000/admin-check-safe?token=garbled"
   ```
   Returns 403 denied plus a `ref` ID. The error was caught, logged, and denied rather than silently passed through.

5. Confirm the server log contains the correlation ID from step 4:
   ```
   docker compose logs vuln-app | grep "auth error"
   ```

**Expected output:** Step 1 returns a raw error string with internals; step 2 returns a generic message with a `ref` ID.

**Verify completion:** The `ref` ID from the client response appears in `docker compose logs vuln-app`. The client learns nothing about internals.

**Cleanup:** `docker compose down`

---

## Lab 6 - Wire Bandit into a CI Gate

**Objective:** Run SAST (Bandit), a custom Semgrep rule, and SCA (pip-audit) against the vulnerable app source; understand the output; sketch a GitHub Actions gate.

**Setup:** Use the tools container (which has Bandit, Semgrep, and pip-audit pre-installed, and the app source at `/target`):

```
docker compose --profile tools run --rm tools bash
```

**Walkthrough (inside the tools container):**

1. Run Bandit against the app source:
   ```
   bandit -r /target -ll
   ```
   Map each finding to a vulnerability class from Chapter 3: `B608` is SQL injection, `B602` is `shell=True`.

2. Run the custom Semgrep rule that catches raw-SQL f-strings (Bandit can miss some patterns):
   ```
   semgrep --config /semgrep/sql-fstring.yaml /target
   ```
   Confirm it fires on the vulnerable lines and not on the parameterised equivalents.

3. Run the broader Python ruleset:
   ```
   semgrep --config p/python /target
   ```

4. Run SCA against the app dependencies:
   ```
   pip-audit -r /target/requirements.txt
   ```
   Note any transitive dependency findings - this is the supply-chain blind spot A03:2025 addresses.

5. Back on the host, sketch the GitHub Actions workflow (already shown in Section 3.10.3 of the book) and observe how the non-zero exit code turns a scanner run into a merge gate.

**Expected output:** Bandit reports HIGH-severity findings for `shell=True` and the string-concatenated SQL query. Semgrep fires on the f-string SQL lines. pip-audit reports any known CVEs in the pinned requirements.

**Verify completion:** At least two HIGH-severity Bandit findings map to the injection vulnerabilities from Lab 2. The custom Semgrep rule fires on exactly the vulnerable lines and not on the parameterised equivalents.

**Cleanup:**
```
exit   # leave tools container
docker compose down
```

---

## Lab 7 - Threat-Model the Vulnerable App (STRIDE)

**Objective:** Apply the STRIDE mnemonic to the chapter's running example (browser, Flask app, SQLite) and produce a remediation backlog.

**Setup:** `vuln-app` must be running so you can trace the actual data flow. No additional containers are needed.

**Walkthrough:**

1. Sketch the data-flow diagram on paper with three boxes: Browser, Flask App, SQLite database. Draw the trust boundaries as dotted lines at the browser-to-app and app-to-database crossings.

2. Walk the six STRIDE letters for each element and each trust-boundary crossing. For each letter that applies, write one concrete threat. Example starting points:
   - Spoofing on the login endpoint: is this really the authenticated user, or a stolen session?
   - Tampering on the search field: can the user inject SQL into the query?
   - Information Disclosure on the error endpoint: does the server leak internals?
   - Denial of Service on the ping endpoint: can an attacker cause unbounded process spawning?
   - Elevation of Privilege on the admin-check endpoint: can a user reach admin functionality?

3. For each threat, note whether the current code implements a mitigation. The gaps are your remediation backlog. Cross-check against the vulnerable vs safe endpoints you have already tested.

4. Optional: re-draw the model in OWASP Threat Dragon (free, cross-platform: https://owasp.org/www-project-threat-dragon/) and compare its STRIDE-per-element suggestions against your paper version.

**Host setup (not containerised):** OWASP Threat Dragon is an Electron desktop app (macOS, Linux, Windows) or a web app. Install it from https://github.com/OWASP/threat-dragon/releases. The Microsoft Threat Modeling Tool (Windows only) is available at https://aka.ms/threatmodelingtool. Neither needs Docker.

**Expected output:** A table or list with at least one concrete threat per STRIDE letter for the login-to-database flow, each paired with a mitigation (or a conscious acceptance note).

**Verify completion:** Every STRIDE letter has a threat entry and a mitigation. At least one gap in the current code is identified that corresponds to a vulnerability demonstrated in Labs 2-5.

**Cleanup:** `docker compose down`

---

## Teardown

Remove all containers, images built by this compose file, and networks:

```
docker compose down --rmi local -v
```

---

## Safety and Authorisation Note

All offensive steps in these labs (SQL injection, command injection, JWT forgery) target only the containers defined in this compose file running on `127.0.0.1`. Do NOT point these tools at any system you do not own or have explicit written permission to test. Unauthorised scanning and exploitation is unlawful in most jurisdictions; see Chapter 9 (Law, Ethics, Professional Practice, and AI Governance) for the full treatment.

The vulnerable app binds exclusively to `127.0.0.1:5000` - it will not accept connections from other hosts when run with this compose file.
