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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ git clone https://github.com/cerberauth/api-vulns-challenges.git

4. Exploit the Vulnerability: Once the environment is set up, attempt to exploit the vulnerability as per the challenge instructions. Document your findings and the steps you took.

### Vulnerable vs. fixed mode

Every challenge server can run in two modes, toggled with the `--vulnerable` flag on its `serve` command (`--vulnerable=true` by default): the vulnerable mode reproduces the flaw the challenge is named after, while `--vulnerable=false` runs the fixed, non-vulnerable implementation of the same API. This lets you validate a scanner or exploit against the vulnerable server, then confirm it no longer works against the fixed one. See each challenge's README for the specific behavior difference between modes.

5. Share Your Results: If you wish, you can share your findings, write-ups, or solutions by submitting a pull request to this repository.

## Challenges
Expand Down
9 changes: 9 additions & 0 deletions challenges/apollo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ This challenge demonstrates an Apollo GraphQL server.
npm install && npm start
```

## Modes

The server supports two modes, toggled with the `VULNERABLE` environment variable (defaults to `true`):

```bash
VULNERABLE=true npm start # vulnerable: introspection enabled, CORS allows any origin
VULNERABLE=false npm start # fixed: introspection disabled, CORS restricted to a trusted origin
```

## 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.
Expand Down
14 changes: 13 additions & 1 deletion challenges/apollo/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ const resolvers = {
},
};

// Toggle between the vulnerable and the fixed, non-vulnerable configuration.
// Defaults to the vulnerable mode, matching the other challenges in this
// repository. Set VULNERABLE=false to run the fixed configuration.
const vulnerable = process.env.VULNERABLE !== 'false';

const app = express();
const httpServer = http.createServer(app);

Expand All @@ -68,13 +73,20 @@ const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
// vulnerable: introspection lets anyone dump the full schema, including
// fields and types never meant to be discoverable by a client
introspection: vulnerable,
});

await server.start();

app.use(
'/graphql',
cors<cors.CorsRequest>(),
// vulnerable: any origin is allowed to make credentialed cross-site
// requests to the GraphQL endpoint
vulnerable
? cors<cors.CorsRequest>()
: cors<cors.CorsRequest>({ origin: 'https://trusted.example.com' }),
helmet(),
express.json(),
logger,
Expand Down
12 changes: 12 additions & 0 deletions challenges/auth-not-verified/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ This challenge demonstrates an API where authentication is not verified.
go run main.go
```

## Modes

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

```bash
# vulnerable: any request is accepted, no authentication is checked at all
go run main.go serve --vulnerable=true

# fixed: a bearer token is required
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.
Expand Down
10 changes: 9 additions & 1 deletion challenges/auth-not-verified/serve/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,17 @@ import (
"github.com/cerberauth/api-vulns-challenges/common"
)

func RunServer(port string) {
func RunServer(port string, vulnerable bool) {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if !vulnerable {
// fixed: a bearer token is required and actually checked
if _, ok := common.ExtractBearerToken(r); !ok {
w.WriteHeader(401)
return
}
}

w.WriteHeader(204)
})

Expand Down
12 changes: 12 additions & 0 deletions challenges/discoverable/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ This challenge demonstrates an API that is discoverable through an OpenAPI speci
go run main.go
```

## Modes

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

```bash
# vulnerable: the OpenAPI spec under ./static is served publicly
go run main.go serve --vulnerable=true

# fixed: only the health endpoints are exposed, the spec is not served
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.
Expand Down
11 changes: 8 additions & 3 deletions challenges/discoverable/serve/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ import (
"github.com/cerberauth/api-vulns-challenges/common"
)

func RunServer(port string) {
func RunServer(port string, vulnerable bool) {
mux := http.NewServeMux()
fs := http.FileServer(http.Dir("./static"))
mux.Handle("/", fs)

if vulnerable {
// vulnerable: the OpenAPI spec (and any other file under ./static)
// is served publicly, letting anyone enumerate the full API surface
fs := http.FileServer(http.Dir("./static"))
mux.Handle("/", fs)
}

mux.HandleFunc("/health/ready", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
Expand Down
12 changes: 12 additions & 0 deletions challenges/http-misconfigurations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ This challenge demonstrates various HTTP misconfigurations, including CORS, CSP,
go run main.go
```

## Modes

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

```bash
# vulnerable: method override headers bypass method checks, CORS is wide open, cookies miss Secure/HttpOnly/SameSite/expiration, CSP allows framing
go run main.go serve --vulnerable=true

# fixed: each endpoint returns its hardened counterpart
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.
Expand Down
40 changes: 31 additions & 9 deletions challenges/http-misconfigurations/serve/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (
"time"
)

func RunServer(port string) {
func RunServer(port string, vulnerable bool) {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNoContent)
Expand All @@ -20,7 +20,12 @@ func RunServer(port string) {
}

w.Header().Set("Content-Type", "application/json")
if r.Method == http.MethodGet || r.Header.Get("X-HTTP-Method-Override") == http.MethodGet || r.URL.Query().Get("_method") == http.MethodGet {
// vulnerable: a GET-only endpoint can also be reached with the real
// method overridden via a header or query parameter, which lets an
// attacker bypass method-based access controls (e.g. a proxy/WAF
// rule that only inspects r.Method)
overridden := vulnerable && (r.Header.Get("X-HTTP-Method-Override") == http.MethodGet || r.URL.Query().Get("_method") == http.MethodGet)
if r.Method == http.MethodGet || overridden {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"message": "GET method"}`))
} else {
Expand All @@ -30,13 +35,22 @@ func RunServer(port string) {

http.HandleFunc("/headers/cors-wildcard", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
if vulnerable {
w.Header().Set("Access-Control-Allow-Origin", "*")
} else {
w.Header().Set("Access-Control-Allow-Origin", "https://trusted.example.com")
w.Header().Set("Vary", "Origin")
}
w.WriteHeader(http.StatusNoContent)
})

http.HandleFunc("/headers/csp-frame-ancestors", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Security-Policy", "frame-ancestors 'http://example.com'")
if vulnerable {
w.Header().Set("Content-Security-Policy", "frame-ancestors 'http://example.com'")
} else {
w.Header().Set("Content-Security-Policy", "frame-ancestors 'none'")
}
w.WriteHeader(http.StatusNoContent)
})

Expand All @@ -45,7 +59,7 @@ func RunServer(port string) {
Name: "unsecure",
Value: "unsecure",
SameSite: http.SameSiteStrictMode,
Secure: false,
Secure: !vulnerable,
HttpOnly: true,
Expires: time.Now().Add(24 * time.Hour),
})
Expand All @@ -58,7 +72,7 @@ func RunServer(port string) {
Name: "unsecure",
Value: "unsecure",
SameSite: http.SameSiteStrictMode,
HttpOnly: false,
HttpOnly: !vulnerable,
Secure: true,
Expires: time.Now().Add(24 * time.Hour),
})
Expand All @@ -67,10 +81,14 @@ func RunServer(port string) {
})

http.HandleFunc("/cookies/samesite-none", func(w http.ResponseWriter, r *http.Request) {
sameSite := http.SameSiteNoneMode
if !vulnerable {
sameSite = http.SameSiteStrictMode
}
http.SetCookie(w, &http.Cookie{
Name: "unsecure",
Value: "unsecure",
SameSite: http.SameSiteNoneMode,
SameSite: sameSite,
HttpOnly: true,
Secure: true,
Expires: time.Now().Add(24 * time.Hour),
Expand All @@ -80,13 +98,17 @@ func RunServer(port string) {
})

http.HandleFunc("/cookies/no-expiration", func(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, &http.Cookie{
cookie := &http.Cookie{
Name: "unsecure",
Value: "unsecure",
SameSite: http.SameSiteStrictMode,
HttpOnly: true,
Secure: true,
})
}
if !vulnerable {
cookie.Expires = time.Now().Add(24 * time.Hour)
}
http.SetCookie(w, cookie)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusNoContent)
})
Expand Down
11 changes: 10 additions & 1 deletion challenges/jwt-alg-none-bypass/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@ This challenge demonstrates a JWT implementation that is vulnerable to the 'none
## How to run it

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

## Modes

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

```bash
go run main.go serve --vulnerable=true # vulnerable: accepts tokens signed with alg "none"
go run main.go serve --vulnerable=false # fixed: only HMAC-signed tokens are accepted, alg "none" is rejected
```

## Disclaimer
Expand Down
2 changes: 1 addition & 1 deletion challenges/jwt-alg-none-bypass/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"github.com/golang-jwt/jwt/v5"
)

func generateToken() (string, error) {
func generateToken(vulnerable bool) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodNone, jwt.MapClaims{
"sub": "2cb307ba-bb46-4194-854f-4774046d9c9b",
"name": "John Doe",
Expand Down
4 changes: 2 additions & 2 deletions challenges/jwt-alg-none-bypass/serve/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"github.com/golang-jwt/jwt/v5"
)

func RunServer(port string) {
func RunServer(port string, vulnerable bool) {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
tokenString, ok := common.ExtractBearerToken(r)
Expand All @@ -20,7 +20,7 @@ func RunServer(port string) {

token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// fake vulnerability
if token.Method.Alg() == "none" {
if vulnerable && token.Method.Alg() == "none" {
return jwt.UnsafeAllowNoneSignatureType, nil
}

Expand Down
9 changes: 9 additions & 0 deletions challenges/jwt-apple-token-relay/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ This challenge demonstrates a token cross-service relay attack against a relying
go run main.go serve
```

## Modes

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

```bash
go run main.go serve --vulnerable=true # vulnerable: the audience (`aud`) claim is never checked
go run main.go serve --vulnerable=false # fixed: the token must also match the expected audience
```

## How to exploit it

```bash
Expand Down
9 changes: 7 additions & 2 deletions challenges/jwt-apple-token-relay/serve/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const (
VictimAudience = "com.victim.app"
)

func RunServer(port string) {
func RunServer(port string, vulnerable bool) {
cwd, err := os.Getwd()
if err != nil {
log.Fatal(err)
Expand All @@ -43,12 +43,17 @@ func RunServer(port string) {
// The relying party checks the Apple signature and issuer only.
// It never validates the audience, so an ID token minted by Apple
// for a different, attacker-controlled app is accepted here too.
parserOpts := []jwt.ParserOption{jwt.WithIssuer(Issuer)}
if !vulnerable {
parserOpts = append(parserOpts, jwt.WithAudience(VictimAudience))
}

token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return idpPublicKey, nil
}, jwt.WithIssuer(Issuer))
}, parserOpts...)

if err != nil || !token.Valid {
fmt.Println(err)
Expand Down
12 changes: 12 additions & 0 deletions challenges/jwt-blank-secret/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ This challenge demonstrates a JWT implementation that uses a blank secret for si
go run main.go
```

## Modes

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

```bash
# vulnerable: tokens signed with a blank HMAC secret are accepted
go run main.go serve --vulnerable=true

# fixed: a strong, high-entropy HMAC secret is required
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.
Expand Down
7 changes: 5 additions & 2 deletions challenges/jwt-blank-secret/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ import (
"github.com/golang-jwt/jwt/v5"
)

func generateToken() (string, error) {
func generateToken(vulnerable bool) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": "2cb307ba-bb46-4194-854f-4774046d9c9b",
"name": "John Doe",
"iat": time.Now().Unix(),
"exp": time.Now().Add(time.Hour).Unix(),
})
return token.SignedString([]byte(""))
if vulnerable {
return token.SignedString([]byte(""))
}
return token.SignedString([]byte(serve.Secret))
}

func main() {
Expand Down
Loading
Loading