Skip to content

Add Season 5: Mission Control (SSRF) and Paperwork (Broken Access Control) - #216

Open
ggeorgeazevedo wants to merge 6 commits into
skills:mainfrom
ggeorgeazevedo:season-5-mission-control-ssrf
Open

Add Season 5: Mission Control (SSRF) and Paperwork (Broken Access Control)#216
ggeorgeazevedo wants to merge 6 commits into
skills:mainfrom
ggeorgeazevedo:season-5-mission-control-ssrf

Conversation

@ggeorgeazevedo

@ggeorgeazevedo ggeorgeazevedo commented Aug 19, 2026

Copy link
Copy Markdown

Summary

Follows up on the proposal in #213. Per @xcorail's guidance there, this is opened as a new season rather than as new challenges inside Season 2, and it now carries the two levels @xcorail green-lit in that thread.

Both cover priority combinations listed in CONTRIBUTING.md, and both share a shape: the code is not missing a security check, it has one that looks reasonable and does not hold.

Changes

  • Season-5/ — a new self-contained season with its own README.md, written so further community levels can be appended as Level-3, Level-4, … (the open Lua proposal in Adding New Level: Lua Metatable hooks #128 would slot straight in, which I think also matches @jkcso's "appendix" idea).
  • Season-5/Level-1/Mission Control, Server-Side Request Forgery (CWE-918) in javascript.
  • Season-5/Level-2/Paperwork, Broken Access Control (CWE-639 / CWE-915 / CWE-602) in typescript.
  • One row in the Secure Coding table of the root README.md.

Each level follows the existing file conventions: code, hack, tests, solution, two hint files, and its own package.json.


Level 1 — Mission Control (SSRF, javascript)

A space agency dashboard previews telemetry feeds from partner observatories, and lets an operator paste the URL of a partner that is not registered yet.

The bug. code.js screens that URL against a blocklist of strings (localhost, 127.0.0.1, 169.254.169.254, metadata). The check runs once, on the raw string, before anything is fetched. hack.js gets past it three ways:

  1. IPv4 shorthandhttp://127.1:<port>/admin/keys reaches the internal admin API.
  2. Hex encoded addresshttp://0x7f000001:<port>/latest/meta-data/iam/credentials reaches the stand-in metadata service.
  3. Redirect — a URL that passes every check answers 302 to http://localhost:<port>/admin/keys, and fetch follows it automatically.

The fix. solution.js stops letting the caller pick the destination: the request carries a feed id, resolved against the server-side registry that already exists at the top of the file, and redirects are handled manually so the response has to come back from the host that was allowed. A closing comment points at what a service that genuinely must fetch arbitrary URLs would need on top — resolve the hostname, reject private, loopback and link-local ranges, and pin the connection to the address that was validated.

Level 2 — Paperwork (Broken Access Control, typescript)

An expense reimbursement API where the authentication is solid and the authorization is missing. Alice and Bob are employees, Carol manages them both. Every endpoint knows exactly who is calling; not one of them checks what that person is allowed to reach.

The bugs. One per endpoint, each a different flavour of the same mistake:

  1. GET /reports/:id — verifies the session, never the ownership. The comment in the code argues the ids are random so nobody can land on someone else's report, which is an argument about how hard a record is to find, not about who may read it. Bob pulls Alice's payout details without guessing anything.
  2. PATCH /reports/:id — does check ownership, then does Object.assign(report, req.body). Owning a record is not owning every field of it: Alice approves her own report by sending status: "approved".
  3. POST /reports/:id/approve — reads the role from an x-user-role header "to save a lookup". Alice sends x-user-role: manager and signs off her own spending.

The fix. solution.ts fixes each one at the level it belongs: a single mayRead helper both read paths go through, an allow list of editable fields that only applies while the report is a draft, and the role read from the server-side user record — plus the rule that a manager never approves their own report.


Verification

$ npm install Season-5/Level-1/
$ mocha Season-5/Level-1/tests.js     # 5 passing against code.js
$ mocha Season-5/Level-1/hack.js      # 3 failing against code.js
# after switching TARGET to ./solution
$ mocha Season-5/Level-1/tests.js     # 5 passing
$ mocha Season-5/Level-1/hack.js      # 3 passing

$ cd Season-5/Level-2 && npm install
$ npx tsc --noEmit                    # clean
$ npm test                            # 5 passing against code.ts
$ npm run hack                        # 3 failing against code.ts
# after switching the import to ./solution
$ npm test                            # 5 passing
$ npm run hack                        # 3 passing

Neither level needs outbound network access. Level 1 starts the partner observatory and the internal services on ephemeral loopback ports; Level 2 is entirely in memory. Both are deterministic and work offline inside Codespaces.

Notes for reviewers

  • The naming is a placeholder. I went with Season-5 because it is the next number, but CONTRIBUTING.md earmarks Season 5 for the ProdBot/AI track. Renaming the folder is trivial, so if you would rather call this a bonus season, or reserve 5 for AI, just tell me and I will push the rename.
  • Level 2 is genuinely TypeScript, with its own tsconfig.json and ts-node, since there is no TS anywhere in the game yet. The trade-off is that it runs from inside its own folder (cd Season-5/Level-2 && npm test) rather than from the repo root like the JS levels; the README says so explicitly. Happy to convert it to plain JavaScript if you would rather keep one way of running things.
  • Level order is easy to change. I would put Paperwork slightly below Mission Control in difficulty — it needs no encoding trivia, just the discipline of asking "allowed to do this, to this object?" — so if you would prefer them swapped, that is a rename away.
  • Toggling between code and solution in Level 1 is done with a TARGET constant at the top of tests.js / hack.js instead of the two commented require lines used elsewhere, because the module has to be required inside before() once the mock servers have picked their ports. Let me know if you would rather keep the exact style of the other levels.
  • .devcontainer/devcontainer.json installs dependencies for Seasons 3 and 4 in postCreateCommand but not for this season, so players run npm install themselves as the READMEs instruct. Say the word if you would like me to add Season 5 to that command.

Closes: #213

Task list

Vulnerable Express app, three SSRF exploits, unit tests, one solution and two hints. Runs offline: the partner feed and the internal services are started on ephemeral loopback ports by the test files.
An expense reimbursement API in TypeScript with solid authentication and no authorization: a missing ownership check on read, a blind object merge on update, and a role taken from a client-supplied header on approval. Ships with three exploits, unit tests, one solution, two hints and its own tsconfig.
@ggeorgeazevedo ggeorgeazevedo changed the title Add Season 5 - Level 1: Mission Control (SSRF in JavaScript/Node.js) Add Season 5: Mission Control (SSRF) and Paperwork (Broken Access Control) Aug 20, 2026
@ggeorgeazevedo

Copy link
Copy Markdown
Author

Update: @xcorail gave the go-ahead in #213 for a second level, so I've pushed it to this same branch rather than opening a separate PR — the season lands as one reviewable unit instead of two PRs waiting on each other.

Season 5 - Level 2: Paperwork — Broken Access Control in TypeScript. An expense reimbursement API where the authentication is solid and the authorization is missing: a read endpoint that checks the session but not the ownership, an update endpoint that merges the request body straight into the record, and an approval endpoint that takes the caller's role from a request header. Three exploits, unit tests, one solution and two hints, all in memory so it runs offline.

I've rewritten the description above to cover both levels, including verification steps and a few open questions for you — season naming, level order, and whether you'd rather have Level 2 in plain JavaScript than TypeScript.

No rush on review at all, and thanks again for the quick steer on the season structure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant