Skip to content
Merged
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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,22 @@ The following challenges are available in this repository:
- [JWT Weak HMAC Secret](challenges/jwt-weak-hmac-secret)
- [JWT Weak RSA Key](challenges/jwt-weak-rsa-key)
- [JWT x5c/x5u Header Injection](challenges/jwt-x5c-x5u-header-injection)
- [Nginx Alias Traversal](challenges/nginx-alias-traversal)
- [Proxy Cache Deception](challenges/proxy-cache-deception)
- [Proxy Client-IP Spoofing](challenges/proxy-client-ip-spoofing)
- [Proxy Config Static Analysis](challenges/proxy-config-static-analysis)
- [Proxy CORS Misconfiguration](challenges/proxy-cors-misconfiguration)
- [Proxy Host Header Injection](challenges/proxy-host-header-injection)
- [Proxy HTTP/2 Authority Spoofing](challenges/proxy-http2-authority-spoofing)
- [Proxy Information Disclosure](challenges/proxy-info-disclosure)
- [Proxy Open SSRF](challenges/proxy-open-ssrf)
- [Proxy Path Bypass](challenges/proxy-path-bypass)
- [Proxy Rate Limit Bypass](challenges/proxy-rate-limit-bypass)
- [Proxy Request Smuggling](challenges/proxy-request-smuggling)
- [Proxy Security Headers](challenges/proxy-security-headers)
- [Proxy Template Injection](challenges/proxy-template-injection)
- [Proxy TLS Misconfiguration](challenges/proxy-tls-misconfiguration)
- [Proxy WAF Bypass](challenges/proxy-waf-bypass)
- [Strong API Key](challenges/strong-api-key)
- [Strong HTTP Basic](challenges/strong-http-basic)

Expand Down
14 changes: 14 additions & 0 deletions challenges/nginx-alias-traversal/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM nginx:1.27-alpine

COPY challenges/nginx-alias-traversal/conf/nginx.vulnerable.conf /etc/nginx/conf.d/nginx.vulnerable.conf
COPY challenges/nginx-alias-traversal/conf/nginx.fixed.conf /etc/nginx/conf.d/nginx.fixed.conf
COPY challenges/nginx-alias-traversal/html/ /usr/share/nginx/html/
COPY challenges/nginx-alias-traversal/docker-entrypoint.sh /docker-entrypoint.sh

RUN chmod +x /docker-entrypoint.sh

EXPOSE 8080

ENV VULNERABLE=true

ENTRYPOINT ["/docker-entrypoint.sh"]
35 changes: 35 additions & 0 deletions challenges/nginx-alias-traversal/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Nginx-Specific Misconfigurations

This challenge demonstrates the classic Nginx off-by-slash alias traversal: a `location` directive without a trailing slash combined with an `alias` directive lets a path like `/files../secret/flag.txt` escape the intended alias root, because Nginx strips only the literal `/files` prefix and appends the remainder verbatim to the alias path.

## How to run it

```bash
docker build -f Dockerfile -t nginx-alias-traversal ../..
docker run --rm -p 8080:8080 nginx-alias-traversal
```

## Modes

Unlike the Go-based challenges in this repository, this challenge is a real Nginx server, so the mode is toggled with the `VULNERABLE` environment variable at container startup (defaults to `true`):

```bash
# vulnerable: location /files (no trailing slash) + alias escapes the alias root
docker run --rm -p 8080:8080 -e VULNERABLE=true nginx-alias-traversal

# fixed: location /files/ (trailing slash) requires the path to stay under /files/
docker run --rm -p 8080:8080 -e VULNERABLE=false nginx-alias-traversal
```

```bash
curl http://localhost:8080/files/index.html # public file, always reachable
curl http://localhost:8080/files../secret/flag.txt # only reachable in vulnerable mode
```

## Disclaimer

The challenges provided in this repository are designed to be educational and for testing purposes only. Do not attempt to exploit vulnerabilities in systems or APIs without proper authorization. Always ensure that you have the necessary permissions to conduct security testing on any system or application.

---

Learn more about API security at [Cerberauth](https://www.cerberauth.com/)
19 changes: 19 additions & 0 deletions challenges/nginx-alias-traversal/conf/nginx.fixed.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
worker_processes 1;
events { worker_connections 1024; }

http {
include mime.types;
default_type application/octet-stream;

server {
listen 8080;
server_name localhost;

# fixed: the location has a trailing slash matching the alias, so a
# request must start with "/files/" to match this block at all,
# closing off the off-by-slash escape.
location /files/ {
alias /usr/share/nginx/html/public/;
}
}
}
21 changes: 21 additions & 0 deletions challenges/nginx-alias-traversal/conf/nginx.vulnerable.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
worker_processes 1;
events { worker_connections 1024; }

http {
include mime.types;
default_type application/octet-stream;

server {
listen 8080;
server_name localhost;

# vulnerable: the location has no trailing slash while alias does.
# nginx strips the "/files" prefix and appends the remainder
# verbatim to the alias path, so a request like "/files../secret/"
# resolves to "/usr/share/nginx/html/public/../secret/", escaping
# the intended alias root entirely.
location /files {
alias /usr/share/nginx/html/public/;
}
}
}
12 changes: 12 additions & 0 deletions challenges/nginx-alias-traversal/docker-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/sh
set -e

VULNERABLE="${VULNERABLE:-true}"

if [ "$VULNERABLE" = "true" ]; then
cp /etc/nginx/conf.d/nginx.vulnerable.conf /etc/nginx/nginx.conf
else
cp /etc/nginx/conf.d/nginx.fixed.conf /etc/nginx/nginx.conf
fi

exec nginx -g "daemon off;"
7 changes: 7 additions & 0 deletions challenges/nginx-alias-traversal/html/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<!DOCTYPE html>
<html>
<body>
<h1>Public files</h1>
<p>This directory only contains public, non-sensitive files.</p>
</body>
</html>
1 change: 1 addition & 0 deletions challenges/nginx-alias-traversal/html/secret/flag.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This file lives outside the /files public alias root and must never be reachable through it.
14 changes: 14 additions & 0 deletions challenges/proxy-cache-deception/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

proxy-cache-deception
22 changes: 22 additions & 0 deletions challenges/proxy-cache-deception/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
FROM golang:1.26 AS builder

WORKDIR /app

COPY common/ ./common/
COPY challenges/proxy-cache-deception/ ./challenges/proxy-cache-deception/

WORKDIR /app/challenges/proxy-cache-deception
RUN CGO_ENABLED=0 GOWORK=off GOOS=linux go build -o /proxy-cache-deception .

FROM gcr.io/distroless/static-debian11:nonroot AS runner

WORKDIR /

COPY --from=builder --chown=nonroot:nonroot /proxy-cache-deception /usr/bin/proxy-cache-deception

EXPOSE 8080

USER nonroot:nonroot

ENTRYPOINT ["proxy-cache-deception"]
CMD ["serve"]
34 changes: 34 additions & 0 deletions challenges/proxy-cache-deception/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Caching Behavior & Web Cache Deception

This challenge demonstrates a shared cache that keys authenticated responses by path only (ignoring the `Authorization` header), and a web cache deception scenario where an authenticated endpoint's data is exposed under a static-looking path (`/assets/profile.js`) and gets cached and replayed to any caller.

## How to run it

```bash
go run main.go serve
```

## Endpoints

- `GET /account` — returns data scoped to the caller's `Authorization` token
- `GET /assets/profile.js` — static-looking path backed by authenticated data

## Modes

The server supports two modes, toggled with the `--vulnerable` flag on the `serve` command (defaults to `true`):

```bash
# vulnerable: the cache key ignores Authorization, so the first caller's response leaks to everyone; /assets/profile.js caches and serves authenticated data
go run main.go serve --vulnerable=true

# fixed: the cache key includes the Authorization token, and authenticated content is never cached or exposed under a static path
go run main.go serve --vulnerable=false
```

## Disclaimer

The challenges provided in this repository are designed to be educational and for testing purposes only. Do not attempt to exploit vulnerabilities in systems or APIs without proper authorization. Always ensure that you have the necessary permissions to conduct security testing on any system or application.

---

Learn more about API security at [Cerberauth](https://www.cerberauth.com/)
13 changes: 13 additions & 0 deletions challenges/proxy-cache-deception/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module github.com/cerberauth/api-vulns-challenges/challenges/proxy-cache-deception

go 1.26

require github.com/spf13/cobra v1.10.2 // indirect

require (
github.com/cerberauth/api-vulns-challenges/common v0.0.0-00010101000000-000000000000
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
)

replace github.com/cerberauth/api-vulns-challenges/common => ../../common
11 changes: 11 additions & 0 deletions challenges/proxy-cache-deception/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
10 changes: 10 additions & 0 deletions challenges/proxy-cache-deception/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package main

import (
"github.com/cerberauth/api-vulns-challenges/challenges/proxy-cache-deception/serve"
"github.com/cerberauth/api-vulns-challenges/common"
)

func main() {
common.Execute(serve.RunServer)
}
100 changes: 100 additions & 0 deletions challenges/proxy-cache-deception/serve/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package serve

import (
"fmt"
"log"
"net/http"
"strings"
"sync"
)

type cacheEntry struct {
body string
contentType string
}

// cache simulates a shared edge cache in front of the backend.
type cache struct {
mu sync.Mutex
store map[string]cacheEntry
}

func newCache() *cache {
return &cache{store: make(map[string]cacheEntry)}
}

func RunServer(port string, vulnerable bool) {
c := newCache()

// /account returns data scoped to the caller's Authorization token.
http.HandleFunc("/account", func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")

// vulnerable: the cache key only considers the path, ignoring the
// Authorization header, so the first caller's authenticated response
// gets served to every subsequent caller regardless of their token
key := r.URL.Path
if !vulnerable {
key = r.URL.Path + "|" + token
}

c.mu.Lock()
entry, hit := c.store[key]
c.mu.Unlock()

if hit {
w.Header().Set("Content-Type", entry.contentType)
w.Header().Set("X-Cache", "HIT")
w.Write([]byte(entry.body))
return
}

body := fmt.Sprintf(`{"email": "%s@example.com"}`, strings.TrimPrefix(token, "Bearer "))
entry = cacheEntry{body: body, contentType: "application/json"}

c.mu.Lock()
c.store[key] = entry
c.mu.Unlock()

w.Header().Set("Content-Type", entry.contentType)
w.Header().Set("X-Cache", "MISS")
w.Write([]byte(entry.body))
})

// /assets/profile.js simulates web cache deception: an authenticated
// endpoint's content is exposed through a path that looks like a static
// asset, which shared caches will happily cache and serve to anyone.
http.HandleFunc("/assets/profile.js", func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
w.Header().Set("Content-Type", "application/javascript")

if vulnerable {
// cached purely by path, so the response containing the first
// authenticated caller's data is reused for anonymous requests
key := r.URL.Path
c.mu.Lock()
entry, hit := c.store[key]
c.mu.Unlock()
if hit {
w.Header().Set("X-Cache", "HIT")
w.Write([]byte(entry.body))
return
}
body := fmt.Sprintf("var profile = {token: %q};", token)
c.mu.Lock()
c.store[key] = cacheEntry{body: body}
c.mu.Unlock()
w.Header().Set("X-Cache", "MISS")
w.Write([]byte(body))
return
}

// fixed: authenticated content is never cached, and is not served
// under a static-looking path in the first place
w.Header().Set("Cache-Control", "no-store")
http.NotFound(w, r)
})

log.Println("Server started at port", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
14 changes: 14 additions & 0 deletions challenges/proxy-client-ip-spoofing/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

proxy-client-ip-spoofing
22 changes: 22 additions & 0 deletions challenges/proxy-client-ip-spoofing/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
FROM golang:1.26 AS builder

WORKDIR /app

COPY common/ ./common/
COPY challenges/proxy-client-ip-spoofing/ ./challenges/proxy-client-ip-spoofing/

WORKDIR /app/challenges/proxy-client-ip-spoofing
RUN CGO_ENABLED=0 GOWORK=off GOOS=linux go build -o /proxy-client-ip-spoofing .

FROM gcr.io/distroless/static-debian11:nonroot AS runner

WORKDIR /

COPY --from=builder --chown=nonroot:nonroot /proxy-client-ip-spoofing /usr/bin/proxy-client-ip-spoofing

EXPOSE 8080

USER nonroot:nonroot

ENTRYPOINT ["proxy-client-ip-spoofing"]
CMD ["serve"]
Loading
Loading