SourCode a FreeFungi app

Challenges FF-005

Sherbet CWE-425 A01:2021

The Staff Room

Missing access control on /admin/export.

The staff back office at /admin is properly locked: log in as a normal customer and you get bounced. But the developers protected the page they were thinking about and forgot the one next to it. An admin export endpoint sits wide open, and it hands the whole customer list to anyone who asks.

New to this? What is forced browsing (broken access control)?

Forced browsing is just requesting a URL directly instead of following a link to it. It becomes a vulnerability when a page or endpoint that should be protected is not, and simply typing its address is enough to reach it.

This is broken access control at the function level: the app guards the obvious admin page but leaves a related admin function unguarded. Guessable paths and unlinked “hidden” pages are found in seconds by anyone who looks.

No account needed, and no tools either: your browser is enough. A directory brute-forcer only makes the guessing faster, so it is kept as an optional hard-mode step at the end. Make sure the shop is running at http://localhost:3000.

1 · Find it

Try to reach the back office without logging in and you are turned away:

GET /admin      -> 302, redirected to /login

So the obvious door is locked. Now go looking for others. Admin areas rarely have just one endpoint, and a back office almost always has an export. Just guess it: type this straight into your browser’s address bar:

http://localhost:3000/admin/export

It answers 200 OK, with no login. That is the whole flaw, found with nothing but a hunch and the address bar.

Hard mode · let a tool find it. Instead of guessing, point a directory brute-forcer at the site with a wordlist and it will turn up unlinked paths like /admin/export for you:

ffuf -u http://localhost:3000/FUZZ -w wordlist.txt

2 · Exploit it

That endpoint answers with no login at all. It returns the full customer list, emails and admin flags included:

{
  "generated_at": "...",
  "customers": [
    { "id": 1, "email": "ada@example.com",   "is_admin": 0, "credit_pence": 0 },
    { "id": 6, "email": "owner@example.com",  "is_admin": 1, "credit_pence": 0 },
    ...
  ]
}

You now have every customer’s email and, helpfully, which of them is an administrator, without ever proving you are staff.

Why it matters: access control has to be checked on every route, not just the front one. Exports, APIs, print views and “internal” tools are exactly where the check gets forgotten.

3 · Understand it

The two handlers sit side by side. One asks for an admin session; the other never got the memo:

// locked
app.get('/admin', requireAdmin, (req, res) => { ... });

// wide open
app.get('/admin/export', (req, res) => {
  res.json({ customers: allCustomers() });
});

Nothing marks the export as sensitive. Access control that lives on individual routes is only as good as the developer’s memory.

4 · Fix it

Put the same guard on the export, and prefer a model where admin areas are protected by default rather than one route at a time:

✗ Vulnerable
app.get('/admin/export',
  (req, res) => { ... });
✓ Patched
// guard the whole prefix
app.use('/admin', requireAdmin);
app.get('/admin/export',
  (req, res) => { ... });

Now every path under /admin, present and future, needs a staff session before it does anything.

The general rule: deny by default. Protect areas, not individual pages, so a new endpoint is safe the moment it exists. Never rely on a URL being unlinked or hard to guess. That is not access control, it is hope.