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
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,24 @@ The following challenges are available in this repository:

- [Apollo GraphQL Server](challenges/apollo)
- [Auth Not Verified](challenges/auth-not-verified)
- [Cache Conditional Validators](challenges/cache-conditional-validators)
- [Cache Control Directive Validation](challenges/cache-control-directive-validation)
- [Cache CPDoS Header Oversize](challenges/cache-cpdos-header-oversize)
- [Cache CPDoS Meta Character](challenges/cache-cpdos-meta-character)
- [Cache CPDoS Method Override](challenges/cache-cpdos-method-override)
- [Cache Deception via Delimiter](challenges/cache-deception-delimiter)
- [Cache Deception via Normalization/Traversal](challenges/cache-deception-normalization)
- [Cache Deception via Static Directory](challenges/cache-deception-static-directory)
- [Cache Deception via Static Extension](challenges/cache-deception-static-extension)
- [Cache Freshness Heuristic Caching](challenges/cache-freshness-heuristic)
- [Cache Key Explosion DoS](challenges/cache-key-explosion-dos)
- [Cache Poisoning via Fat GET](challenges/cache-poisoning-fat-get)
- [Cache Poisoning via Key Collision](challenges/cache-poisoning-key-collision)
- [Cache Poisoning via Parameter Cloaking](challenges/cache-poisoning-parameter-cloaking)
- [Cache Poisoning via Unkeyed Header](challenges/cache-poisoning-unkeyed-header)
- [Cache Sensitive Data Exposure](challenges/cache-sensitive-data-exposure)
- [Cache Stale Directive Bypass](challenges/cache-stale-directive-bypass)
- [Cache Vary Misconfiguration](challenges/cache-vary-misconfiguration)
- [Discoverable](challenges/discoverable)
- [HTTP Misconfigurations](challenges/http-misconfigurations)
- [JWT None Algorithm Bypass](challenges/jwt-alg-none-bypass)
Expand Down
14 changes: 14 additions & 0 deletions challenges/cache-conditional-validators/.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

cache-conditional-validators
22 changes: 22 additions & 0 deletions challenges/cache-conditional-validators/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/cache-conditional-validators/ ./challenges/cache-conditional-validators/

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

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

WORKDIR /

COPY --from=builder --chown=nonroot:nonroot /cache-conditional-validators /usr/bin/cache-conditional-validators

EXPOSE 8080

USER nonroot:nonroot

ENTRYPOINT ["cache-conditional-validators"]
CMD ["serve"]
44 changes: 44 additions & 0 deletions challenges/cache-conditional-validators/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Conditional Request / Validator Misuse

This challenge demonstrates broken cache validators: `/resource` never changes, but the vulnerable configuration issues a brand-new, random `ETag` on every response (a "flapping" validator) and ignores `If-None-Match` / `If-Modified-Since` entirely — so conditional requests never get a `304 Not Modified`, defeating revalidation and forcing a full response on every request.

## How to run it

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

## Endpoint

- `GET /resource` — a static document that should support conditional requests

## Confirming the finding

```bash
# fetch twice and compare ETags - vulnerable: different every time; fixed: stable
curl -s -D - http://localhost:8080/resource -o /dev/null | grep ETag
curl -s -D - http://localhost:8080/resource -o /dev/null | grep ETag

# send a conditional request with the ETag just received
ETAG=$(curl -s -D - http://localhost:8080/resource -o /dev/null | grep -i etag | cut -d' ' -f2 | tr -d '\r')
curl -s -o /dev/null -w "%{http_code}\n" -H "If-None-Match: $ETAG" http://localhost:8080/resource
# expect 200 (vulnerable) vs 304 (fixed)
```

## Modes

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

```bash
# vulnerable: a new random ETag is issued every time and conditional requests are ignored
go run main.go serve --vulnerable=true

# fixed: a stable ETag/Last-Modified is issued and conditional requests are honored with 304
go run main.go serve --vulnerable=false
```

## Disclaimer

This challenge is intentionally vulnerable. Do not deploy it on a publicly accessible server, as this could expose you to attacks.

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

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/cache-conditional-validators/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/cache-conditional-validators/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/cache-conditional-validators/serve"
"github.com/cerberauth/api-vulns-challenges/common"
)

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

import (
"crypto/rand"
"encoding/hex"
"log"
"net/http"
)

const (
fixedBody = "hello, world"
fixedETag = `"a1b2c3d4"`
lastModified = "Wed, 21 Oct 2020 07:28:00 GMT"
contentTypeVl = "text/plain"
)

func randomETag() string {
b := make([]byte, 4)
rand.Read(b)
return `"` + hex.EncodeToString(b) + `"`
}

func RunServer(port string, vulnerable bool) {
// /resource is a static, unchanging document. A well-behaved origin
// should hand out a stable validator (ETag/Last-Modified) and honor
// conditional requests with 304 Not Modified. The vulnerable
// configuration issues a brand-new, random ETag on every response
// (a "flapping" validator) and never checks If-None-Match /
// If-Modified-Since at all, defeating revalidation entirely - every
// conditional request still gets a full 200 response.
http.HandleFunc("/resource", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", contentTypeVl)

if vulnerable {
w.Header().Set("ETag", randomETag())
w.Write([]byte(fixedBody))
return
}

w.Header().Set("ETag", fixedETag)
w.Header().Set("Last-Modified", lastModified)

if inm := r.Header.Get("If-None-Match"); inm != "" && inm == fixedETag {
w.WriteHeader(http.StatusNotModified)
return
}
if ims := r.Header.Get("If-Modified-Since"); ims != "" && ims == lastModified {
w.WriteHeader(http.StatusNotModified)
return
}

w.Write([]byte(fixedBody))
})

log.Println("Server started at port", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
14 changes: 14 additions & 0 deletions challenges/cache-control-directive-validation/.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

cache-control-directive-validation
22 changes: 22 additions & 0 deletions challenges/cache-control-directive-validation/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/cache-control-directive-validation/ ./challenges/cache-control-directive-validation/

WORKDIR /app/challenges/cache-control-directive-validation
RUN CGO_ENABLED=0 GOWORK=off GOOS=linux go build -o /cache-control-directive-validation .

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

WORKDIR /

COPY --from=builder --chown=nonroot:nonroot /cache-control-directive-validation /usr/bin/cache-control-directive-validation

EXPOSE 8080

USER nonroot:nonroot

ENTRYPOINT ["cache-control-directive-validation"]
CMD ["serve"]
37 changes: 37 additions & 0 deletions challenges/cache-control-directive-validation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Conflicting / Invalid Cache-Control Directives

This challenge demonstrates a self-contradictory `Cache-Control` header: `/report` sends `no-store, max-age=3600, public, private` all at once — directives that cannot coexist per RFC 9111 (`no-store` forbids any storage while `max-age` implies storage; `public` and `private` make opposite claims about shared-cache eligibility). Different cache implementations resolve this ambiguity inconsistently, which is itself a risk for sensitive responses.

## How to run it

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

## Endpoint

- `GET /report` — returns a sensitive JSON report

## Confirming the finding

```bash
curl -s -D - http://localhost:8080/report -o /dev/null | grep -i cache-control
```

## Modes

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

```bash
# vulnerable: Cache-Control combines no-store, max-age, public and private in one contradictory header
go run main.go serve --vulnerable=true

# fixed: Cache-Control unambiguously declares no-store only
go run main.go serve --vulnerable=false
```

## Disclaimer

This challenge is intentionally vulnerable. Do not deploy it on a publicly accessible server, as this could expose you to attacks.

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

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/cache-control-directive-validation/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/cache-control-directive-validation/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/cache-control-directive-validation/serve"
"github.com/cerberauth/api-vulns-challenges/common"
)

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

import (
"log"
"net/http"
)

func RunServer(port string, vulnerable bool) {
// /report returns a sensitive report. The vulnerable configuration
// emits a self-contradictory Cache-Control header: "no-store" (never
// persist this response) combined with "max-age=3600" (persist it for
// an hour) and "public, private" (shareable and per-user at once).
// RFC 9111 says a directive parser must be able to flag these as
// invalid/conflicting, and different cache implementations resolve the
// ambiguity inconsistently, which is itself a risk.
http.HandleFunc("/report", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if vulnerable {
w.Header().Set("Cache-Control", "no-store, max-age=3600, public, private")
} else {
w.Header().Set("Cache-Control", "no-store")
}
w.Write([]byte(`{"report": "quarterly figures"}`))
})

log.Println("Server started at port", port)
log.Fatal(http.ListenAndServe(":"+port, nil))
}
14 changes: 14 additions & 0 deletions challenges/cache-cpdos-header-oversize/.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

cache-cpdos-header-oversize
22 changes: 22 additions & 0 deletions challenges/cache-cpdos-header-oversize/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/cache-cpdos-header-oversize/ ./challenges/cache-cpdos-header-oversize/

WORKDIR /app/challenges/cache-cpdos-header-oversize
RUN CGO_ENABLED=0 GOWORK=off GOOS=linux go build -o /cache-cpdos-header-oversize .

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

WORKDIR /

COPY --from=builder --chown=nonroot:nonroot /cache-cpdos-header-oversize /usr/bin/cache-cpdos-header-oversize

EXPOSE 8080

USER nonroot:nonroot

ENTRYPOINT ["cache-cpdos-header-oversize"]
CMD ["serve"]
Loading
Loading