Skip to content

Commit d89bab0

Browse files
committed
feat(lab5): ZAP baseline + auth + Semgrep + correlation + fixed scripts
1 parent 44f7579 commit d89bab0

3 files changed

Lines changed: 131 additions & 17 deletions

File tree

labs/lab5/scripts/compare_zap.sh

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33

44
set -e
55

6-
NOAUTH="${1:-labs/lab5/results/baseline-report.json}"
7-
AUTH="${2:-labs/lab5/results/auth-report.json}"
8-
OUT="${3:-labs/lab5/results/zap-comparison.txt}"
9-
mkdir -p "$(dirname "$OUT")"
6+
NOAUTH="labs/lab5/zap/zap-report-noauth.json"
7+
AUTH="labs/lab5/zap/zap-report-auth.json"
8+
OUT="labs/lab5/analysis/zap-comparison.txt"
9+
mkdir -p labs/lab5/analysis
1010

1111
parse_report() {
1212
local file="$1"
@@ -29,6 +29,8 @@ by_risk = {'3': 0, '2': 0, '1': 0, '0': 0}
2929
risk_names = {'3': 'High', '2': 'Medium', '1': 'Low', '0': 'Info'}
3030
3131
for site in sites:
32+
if 'localhost:3000' not in site.get('@name', ''):
33+
continue
3234
for alert in site.get('alerts', []):
3335
risk = alert.get('riskcode', '0')
3436
by_risk[risk] = by_risk.get(risk, 0) + 1
@@ -41,6 +43,8 @@ for code in ['3','2','1','0']:
4143
# count unique URLs scanned
4244
urls = set()
4345
for site in sites:
46+
if 'localhost:3000' not in site.get('@name', ''):
47+
continue
4448
for alert in site.get('alerts', []):
4549
for inst in alert.get('instances', []):
4650
urls.add(inst.get('uri', ''))

labs/lab5/scripts/zap-auth.yaml

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ env:
22
contexts:
33
- name: "Juice Shop Auth"
44
urls:
5-
- "http://juice-shop:3000"
5+
- "http://localhost:3000"
66
includePaths:
7-
- "http://juice-shop:3000.*"
7+
- "http://localhost:3000.*"
88
excludePaths:
99
- ".*\\.js$"
1010
- ".*\\.css$"
@@ -15,16 +15,16 @@ env:
1515
authentication:
1616
method: "json"
1717
parameters:
18-
loginPageUrl: "http://juice-shop:3000/rest/user/login"
19-
loginRequestUrl: "http://juice-shop:3000/rest/user/login"
18+
loginPageUrl: "http://localhost:3000/rest/user/login"
19+
loginRequestUrl: "http://localhost:3000/rest/user/login"
2020
loginRequestBody: '{"email":"{%username%}","password":"{%password%}"}'
2121
verification:
2222
method: "poll"
2323
loggedInRegex: ".*\"user\".*"
2424
loggedOutRegex: ".*\"error\".*"
2525
pollFrequency: 60
2626
pollUnits: "requests"
27-
pollUrl: "http://juice-shop:3000/rest/user/whoami"
27+
pollUrl: "http://localhost:3000/rest/user/whoami"
2828
pollAdditionalHeaders:
2929
- header: "Authorization"
3030
value: "Bearer {%token%}"
@@ -43,13 +43,13 @@ jobs:
4343
- type: "spider"
4444
parameters:
4545
maxDuration: 5
46-
url: "http://juice-shop:3000"
46+
url: "http://localhost:3000"
4747
user: "admin"
4848

4949
- type: "spiderAjax"
5050
parameters:
5151
maxDuration: 10
52-
url: "http://juice-shop:3000"
52+
url: "http://localhost:3000"
5353
user: "admin"
5454

5555
- type: "passiveScan-config"
@@ -64,19 +64,19 @@ jobs:
6464
- type: "activeScan"
6565
parameters:
6666
user: "admin"
67-
maxScanDurationInMins: 10
68-
maxRuleDurationInMins: 2
67+
maxScanDurationInMins: 15
68+
maxRuleDurationInMins: 3
6969

7070
- type: "report"
7171
parameters:
7272
template: "traditional-html"
73-
reportDir: "/zap/wrk/results/"
74-
reportFile: "auth-report.html"
73+
reportDir: "/zap/wrk/zap/"
74+
reportFile: "report-auth.html"
7575
reportTitle: "ZAP Authenticated Scan Report"
7676

7777
- type: "report"
7878
parameters:
7979
template: "traditional-json"
80-
reportDir: "/zap/wrk/results/"
81-
reportFile: "auth-report.json"
80+
reportDir: "/zap/wrk/zap/"
81+
reportFile: "zap-report-auth.json"
8282
reportTitle: "ZAP Authenticated Scan (JSON)"

submissions/lab5.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Lab 5 — Submission
2+
3+
## Task 1: DAST with OWASP ZAP
4+
5+
### Baseline (unauthenticated) scan
6+
- Duration: 1.5 minutes
7+
- Total alerts: 10
8+
9+
| Severity | Count |
10+
|----------|------:|
11+
| High | 0 |
12+
| Medium | 2 |
13+
| Low | 5 |
14+
| Informational | 3 |
15+
16+
### Authenticated full scan
17+
- Duration: 4.5 minutes
18+
- Total alerts: 12
19+
20+
| Severity | Count |
21+
|----------|------:|
22+
| High | 1 |
23+
| Medium | 4 |
24+
| Low | 3 |
25+
| Informational | 4 |
26+
27+
### The "10–20× more" claim (Lecture 5 slide 11)
28+
- Ratio (auth alerts / baseline alerts): 1.2×
29+
- Did your run match the lecture's ratio?
30+
No, my run showed a ratio of 1.2× for unique alert *types*. The lecture's claim likely refers to the sheer volume of vulnerable *instances* (URLs/parameters), as an authenticated crawler accesses exponentially more pages behind the login. Our script only counted unique risk categories.
31+
- Pick **two specific alerts** that only the authenticated scan found. For each:
32+
1. **SQL Injection (High)**
33+
- Why was it unreachable to the unauthenticated scan? The vulnerable endpoint or parameter is located behind the login barrier, so the baseline crawler couldn't reach the page to inject the payload.
34+
2. **Session ID in URL Rewrite (Medium)**
35+
- Why was it unreachable to the unauthenticated scan? This vulnerability relies on a user session being actively established; since the baseline scan does not log in, it cannot trigger or observe session management flaws.
36+
37+
38+
39+
40+
41+
## Task 2: SAST with Semgrep
42+
43+
### Semgrep severity breakdown
44+
| Severity | Count |
45+
|----------|------:|
46+
| ERROR | 12 |
47+
| WARNING | 10 |
48+
| INFO | 0 |
49+
| **Total** | 22 |
50+
51+
### Top 10 rules by frequency
52+
| Rule ID | Count | OWASP category |
53+
|---------|------:|----------------|
54+
| `javascript.sequelize.security.audit.sequelize-injection-express.express-sequelize-injection` | 6 | A03 |
55+
| `yaml.github-actions.security.run-shell-injection.run-shell-injection` | 5 | A03 |
56+
| `javascript.express.security.audit.express-res-sendfile.express-res-sendfile` | 4 | A01 |
57+
| `javascript.express.security.audit.express-check-directory-listing.express-check-directory-listing` | 4 | A01 |
58+
| `javascript.jsonwebtoken.security.jwt-hardcode.hardcoded-jwt-secret` | 1 | A02 |
59+
| `javascript.express.security.audit.express-open-redirect.express-open-redirect` | 1 | A01 |
60+
| `javascript.lang.security.audit.code-string-concat.code-string-concat` | 1 | A03 |
61+
62+
### Triage shortcut (Lecture 5 slide 8)
63+
Looking at the top 10 — which **one rule** would you fix first if you had time for only one?
64+
I would fix `express-sequelize-injection` first. It has the highest frequency among the vulnerabilities found and represents a severe flaw (SQL Injection - A03), meaning fixing the parameter binding at the ORM level here would quickly eliminate a large cluster of critical vulnerabilities.
65+
66+
### False-positive sample
67+
File path: `data/static/codefixes/unionSqlInjectionChallenge_1.ts`
68+
Rule: `express-sequelize-injection`
69+
Reason: This file is just a static fixture containing code snippets used to display code fixes in the UI; it's not actually executed by the backend, making this finding a false positive.
70+
71+
72+
73+
74+
75+
76+
## Bonus: SAST/DAST Correlation
77+
78+
### Correlation table
79+
| # | OWASP cat | ZAP alert | ZAP URI | Semgrep rule | Semgrep file:line | Confidence |
80+
|---|-----------|-----------|---------|--------------|-------------------|------------|
81+
| 1 | A03 Injection | SQL Injection | `/rest/products/search?q=...` | `express-sequelize-injection` | `routes/search.ts:23` | High (both agree) |
82+
| 2 | A03 Injection | SQL Injection | `/rest/user/login` (email) | `express-sequelize-injection` | `routes/login.ts:34` | High (both agree) |
83+
84+
### Strongest correlation deep-dive
85+
**1. Vulnerable code (from `routes/search.ts:23`)**
86+
```typescript
87+
models.sequelize.query(`SELECT * FROM Products WHERE ((name LIKE '%${criteria}%' OR description LIKE '%${criteria}%') AND deletedAt IS NULL) ORDER BY name`)
88+
```
89+
90+
**2. Working payload (from ZAP report)**
91+
```
92+
URL: http://juice-shop:3000/rest/products/search?q=%27%28
93+
Parameter: q
94+
Attack: '(
95+
```
96+
97+
**3. The fix**
98+
Use parameterized queries (bind variables) instead of string interpolation:
99+
```typescript
100+
models.sequelize.query(
101+
'SELECT * FROM Products WHERE ((name LIKE :criteria OR description LIKE :criteria) AND deletedAt IS NULL) ORDER BY name',
102+
{ replacements: { criteria: `%${criteria}%` } }
103+
)
104+
```
105+
106+
**4. Why both tools caught it**
107+
Semgrep caught it because the code directly interpolates a user-controlled variable (`criteria` from `req.query.q`) into a raw SQL query string inside the `sequelize.query` call—a classic static pattern. ZAP caught it because during its active crawl it injected SQL metacharacters like `'(` into the `q` parameter and observed a database syntax error or unexpected behavior in the response.
108+
109+
### Reflection (2-3 sentences)
110+
Lecture 5 slide 15 calls this "the highest-confidence finding type." In a real PR review, I would want the **SAST finding** first. SAST points exactly to the file and line number (`routes/search.ts:23`), which makes it trivial for a developer to implement a fix immediately. The DAST evidence is then incredibly valuable to prove the exploitability of the finding and to verify that the fix actually resolved the issue at runtime.

0 commit comments

Comments
 (0)