Skip to content

Commit 7cd9eab

Browse files
committed
feat: adicionar 4 exemplos OWASP (sql-injection, xss, ssrf, path-traversal)
Cada exemplo inclui: - vulnerable.py: código com a vulnerabilidade e comentários explicando o risco - hardened.py: versão segura com comentários explicando a correção - README.md: contexto do ataque, fix e como ShieldCode previne em Claude Code Cenários cobertos: SQL Injection, XSS, SSRF e Path Traversal (FastAPI/Python)
1 parent ef17d19 commit 7cd9eab

12 files changed

Lines changed: 403 additions & 0 deletions

File tree

examples/path-traversal/README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Path Traversal Scenario
2+
3+
This FastAPI example downloads invoice PDFs from a storage directory.
4+
5+
## Attack
6+
7+
The vulnerable endpoint accepts:
8+
9+
```text
10+
GET /invoices/download?filename=../../.env
11+
```
12+
13+
`vulnerable.py` joins the raw filename with the invoice directory. `../`
14+
segments can escape the intended folder and read application secrets or system
15+
files if the process has permission.
16+
17+
## Fix
18+
19+
`hardened.py` applies ShieldCode's file security rules:
20+
21+
- Validate filenames with an allowlist regex.
22+
- Resolve the final path before reading it.
23+
- Verify the resolved path stays inside the allowed base directory.
24+
- Require a regular file and return the expected PDF media type.
25+
26+
## ShieldCode in action
27+
28+
Prompt Claude Code after installing ShieldCode:
29+
30+
```text
31+
Create a FastAPI endpoint to download invoice files by filename.
32+
```
33+
34+
With ShieldCode active, Claude should reject arbitrary path input and keep all
35+
file access inside an explicitly approved directory.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from pathlib import Path
2+
import re
3+
4+
from fastapi import FastAPI, HTTPException, Query
5+
from fastapi.responses import FileResponse
6+
7+
app = FastAPI(title="Hardened invoice download")
8+
9+
BASE_DIR = Path("storage/invoices").resolve()
10+
SAFE_INVOICE_NAME = re.compile(r"^[a-zA-Z0-9_-]+\.pdf$")
11+
12+
13+
@app.get("/invoices/download")
14+
def download_invoice(filename: str = Query(..., min_length=5, max_length=80)):
15+
# FIX: allowlist the exact filename format the endpoint supports. Path
16+
# separators, hidden files, and alternate extensions are rejected up front.
17+
if not SAFE_INVOICE_NAME.fullmatch(filename):
18+
raise HTTPException(status_code=400, detail="Invalid invoice filename")
19+
20+
target = (BASE_DIR / filename).resolve()
21+
22+
# FIX: resolve the final path and verify it remains inside BASE_DIR before
23+
# reading. This defends even if symlinks or unusual path segments appear.
24+
if BASE_DIR not in target.parents:
25+
raise HTTPException(status_code=400, detail="Invalid invoice path")
26+
27+
if not target.is_file():
28+
raise HTTPException(status_code=404, detail="Invoice not found")
29+
30+
return FileResponse(
31+
target,
32+
media_type="application/pdf",
33+
filename=filename,
34+
)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from pathlib import Path
2+
3+
from fastapi import FastAPI, HTTPException, Query
4+
from fastapi.responses import FileResponse
5+
6+
app = FastAPI(title="Vulnerable invoice download")
7+
8+
BASE_DIR = Path("storage/invoices")
9+
10+
11+
@app.get("/invoices/download")
12+
def download_invoice(filename: str = Query(...)):
13+
# VULNERABLE: joining untrusted input allows ../ path traversal. A request
14+
# like filename=../../.env can escape storage/invoices and read server files.
15+
target = BASE_DIR / filename
16+
17+
if not target.exists():
18+
raise HTTPException(status_code=404, detail="Invoice not found")
19+
20+
# VULNERABLE: the response returns whatever path the attacker reached.
21+
return FileResponse(target)

examples/sql-injection/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# SQL Injection Scenario
2+
3+
This FastAPI example shows a customer search endpoint that looks normal but
4+
builds SQL by concatenating a query string.
5+
6+
## Attack
7+
8+
The vulnerable endpoint accepts:
9+
10+
```text
11+
GET /customers?q=' OR '1'='1
12+
```
13+
14+
Because `vulnerable.py` inserts `q` directly into the SQL string, the attacker
15+
can change the meaning of the query and return records they should not see.
16+
Verbose database errors also leak schema hints.
17+
18+
## Fix
19+
20+
`hardened.py` applies ShieldCode's SQL injection rules:
21+
22+
- Validate input length with FastAPI `Query`.
23+
- Use parameterized placeholders instead of string formatting.
24+
- Escape `%`, `_`, and `\` before using input in `LIKE`.
25+
- Log internal failures without returning raw database errors.
26+
27+
## ShieldCode in action
28+
29+
Prompt Claude Code after installing ShieldCode:
30+
31+
```text
32+
Write a FastAPI endpoint to search customers by name or email.
33+
```
34+
35+
With ShieldCode active, Claude should avoid f-strings in SQL and produce the
36+
parameterized pattern shown in `hardened.py`.

examples/sql-injection/hardened.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from fastapi import FastAPI, HTTPException, Query
2+
import logging
3+
import sqlite3
4+
5+
app = FastAPI(title="Hardened SQL search")
6+
logger = logging.getLogger("shieldcode.sql")
7+
8+
9+
def get_connection() -> sqlite3.Connection:
10+
connection = sqlite3.connect("shop.db")
11+
connection.row_factory = sqlite3.Row
12+
return connection
13+
14+
15+
def escape_like(value: str) -> str:
16+
# FIX: escape LIKE wildcards so user input stays data, not pattern control.
17+
return value.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
18+
19+
20+
@app.get("/customers")
21+
def search_customers(q: str = Query(..., min_length=1, max_length=80)):
22+
connection = get_connection()
23+
pattern = f"%{escape_like(q)}%"
24+
25+
# FIX: parameterized placeholders keep the SQL structure fixed. The
26+
# database driver sends attacker input as values, not executable SQL.
27+
sql = """
28+
SELECT id, email, full_name
29+
FROM customers
30+
WHERE full_name LIKE ? ESCAPE '\\'
31+
OR email LIKE ? ESCAPE '\\'
32+
ORDER BY full_name
33+
"""
34+
35+
try:
36+
rows = connection.execute(sql, (pattern, pattern)).fetchall()
37+
except sqlite3.DatabaseError as exc:
38+
# FIX: log the internal error server-side, return a generic client
39+
# message, and avoid leaking table or column names.
40+
logger.exception("customer_search_failed")
41+
raise HTTPException(status_code=500, detail="Search failed") from exc
42+
finally:
43+
connection.close()
44+
45+
return {"results": [dict(row) for row in rows]}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
from fastapi import FastAPI, HTTPException, Query
2+
import sqlite3
3+
4+
app = FastAPI(title="Vulnerable SQL search")
5+
6+
7+
def get_connection() -> sqlite3.Connection:
8+
connection = sqlite3.connect("shop.db")
9+
connection.row_factory = sqlite3.Row
10+
return connection
11+
12+
13+
@app.get("/customers")
14+
def search_customers(q: str = Query(..., min_length=1)):
15+
connection = get_connection()
16+
17+
# VULNERABLE: user-controlled input is interpolated directly into SQL.
18+
# An attacker can send q=' OR '1'='1 to turn the WHERE clause into a
19+
# tautology, or append UNION queries to read unrelated tables.
20+
sql = (
21+
"SELECT id, email, full_name FROM customers "
22+
f"WHERE full_name LIKE '%{q}%' OR email LIKE '%{q}%' "
23+
"ORDER BY full_name"
24+
)
25+
26+
try:
27+
rows = connection.execute(sql).fetchall()
28+
except sqlite3.DatabaseError as exc:
29+
# VULNERABLE: returning raw database errors can reveal schema details
30+
# that make injection attacks easier to refine.
31+
raise HTTPException(status_code=500, detail=str(exc)) from exc
32+
finally:
33+
connection.close()
34+
35+
return {"results": [dict(row) for row in rows]}

examples/ssrf/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# SSRF Scenario
2+
3+
This FastAPI example tests whether a webhook URL is reachable.
4+
5+
## Attack
6+
7+
The vulnerable endpoint accepts:
8+
9+
```text
10+
GET /webhook/test?url=http://169.254.169.254/latest/meta-data/
11+
```
12+
13+
Because `vulnerable.py` fetches any URL supplied by the caller, an attacker can
14+
make the server connect to cloud metadata services, localhost admin ports, or
15+
private network hosts that are not reachable from the public internet.
16+
17+
## Fix
18+
19+
`hardened.py` applies ShieldCode's SSRF controls:
20+
21+
- Require `https`.
22+
- Allow only known webhook hosts.
23+
- Reject obvious literal private, loopback, and link-local IPs.
24+
- Disable redirects to avoid allowlist bypasses.
25+
- Return minimal reachability metadata instead of proxying response bodies.
26+
27+
## ShieldCode in action
28+
29+
Prompt Claude Code after installing ShieldCode:
30+
31+
```text
32+
Build a FastAPI endpoint that tests whether a webhook URL is reachable.
33+
```
34+
35+
With ShieldCode active, Claude should ask for or define an allowlist and avoid
36+
fetching arbitrary user-provided URLs.

examples/ssrf/hardened.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from ipaddress import ip_address
2+
from urllib.parse import urlparse
3+
4+
from fastapi import FastAPI, HTTPException, Query
5+
import httpx
6+
7+
app = FastAPI(title="Hardened webhook tester")
8+
9+
ALLOWED_WEBHOOK_HOSTS = {"hooks.stripe.com", "api.github.com", "discord.com"}
10+
11+
12+
def validate_webhook_url(url: str) -> str:
13+
parsed = urlparse(url)
14+
15+
# FIX: allow only HTTPS and an explicit host allowlist. This prevents users
16+
# from turning the API into a proxy for arbitrary internal services.
17+
if parsed.scheme != "https":
18+
raise HTTPException(status_code=400, detail="Webhook URL must use HTTPS")
19+
if parsed.hostname not in ALLOWED_WEBHOOK_HOSTS:
20+
raise HTTPException(status_code=400, detail="Webhook host is not allowed")
21+
22+
# FIX: reject literal private, loopback, and link-local IP addresses. Host
23+
# allowlists are still the main control; this blocks obvious bypass attempts.
24+
try:
25+
host_ip = ip_address(parsed.hostname)
26+
except ValueError:
27+
return url
28+
29+
if host_ip.is_private or host_ip.is_loopback or host_ip.is_link_local:
30+
raise HTTPException(status_code=400, detail="Webhook host is not allowed")
31+
return url
32+
33+
34+
@app.get("/webhook/test")
35+
async def test_webhook(url: str = Query(..., max_length=300)):
36+
safe_url = validate_webhook_url(url)
37+
38+
try:
39+
async with httpx.AsyncClient(
40+
timeout=httpx.Timeout(3.0),
41+
follow_redirects=False,
42+
) as client:
43+
response = await client.get(safe_url)
44+
except httpx.HTTPError as exc:
45+
raise HTTPException(status_code=502, detail="Webhook test failed") from exc
46+
47+
# FIX: return only minimal metadata. Do not proxy response bodies or headers
48+
# from third-party systems back to the caller.
49+
return {"status_code": response.status_code, "reachable": response.is_success}

examples/ssrf/vulnerable.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from fastapi import FastAPI, HTTPException, Query
2+
import httpx
3+
4+
app = FastAPI(title="Vulnerable webhook tester")
5+
6+
7+
@app.get("/webhook/test")
8+
async def test_webhook(url: str = Query(...)):
9+
# VULNERABLE: the server fetches an arbitrary user-supplied URL. Attackers
10+
# can target internal services such as http://169.254.169.254/latest/meta-data
11+
# or http://localhost:8000/admin from the server's network position.
12+
try:
13+
async with httpx.AsyncClient(timeout=5.0) as client:
14+
response = await client.get(url)
15+
except httpx.HTTPError as exc:
16+
raise HTTPException(status_code=502, detail=str(exc)) from exc
17+
18+
# VULNERABLE: reflecting status and body from internal services can expose
19+
# secrets, metadata, or private admin responses.
20+
return {
21+
"status_code": response.status_code,
22+
"headers": dict(response.headers),
23+
"body": response.text[:2000],
24+
}

examples/xss/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Cross-Site Scripting Scenario
2+
3+
This FastAPI example renders a comment preview from submitted form fields.
4+
5+
## Attack
6+
7+
An attacker posts a comment such as:
8+
9+
```html
10+
<img src=x onerror="fetch('/api/session').then(r=>r.text()).then(alert)">
11+
```
12+
13+
`vulnerable.py` inserts that string directly into an HTML response. The browser
14+
parses it as markup and runs the event handler for any user who previews it.
15+
16+
## Fix
17+
18+
`hardened.py` applies ShieldCode's XSS rules:
19+
20+
- Escape user-controlled output with `html.escape`.
21+
- Keep untrusted content as text, not markup.
22+
- Add a restrictive Content Security Policy.
23+
- Send defensive browser headers.
24+
25+
## ShieldCode in action
26+
27+
Prompt Claude Code after installing ShieldCode:
28+
29+
```text
30+
Create a FastAPI route that previews a submitted comment as HTML.
31+
```
32+
33+
With ShieldCode active, Claude should treat every form value as untrusted and
34+
escape it before returning `HTMLResponse`.

0 commit comments

Comments
 (0)