# Web App Security: Breaking Access Control 

I started learning web app hacking yesterday, focused entirely on one OWASP Top 10 category: **Broken Access Control**. By the end of the day I'd solved three PortSwigger Web Security Academy labs, and every single one of them boiled down to the exact same root mistake, just wearing a different costume. Here's what I found, how I found it, and why it matters.

## The One Rule Behind Everything

Before the labs, here's the concept that ties this whole post together:

> The server should never trust the client to tell it who the client is.

Every vulnerability below is a variation of a backend trusting something the browser sent it — a URL, a request body field, a cookie — instead of independently verifying identity and permissions on the server side.

## Lab 1: Unprotected Admin Functionality

**The setup:** A shop app with no visible admin link anywhere in the UI.

**What I tried first:** Guessing `/admin` directly. That returned a 404 — real apps are rarely that obvious.

**What actually worked:** Checking `robots.txt`. This file exists to tell search engine crawlers which pages *not* to index — but it's just plain text, readable by anyone. Sure enough:

```plaintext
User-agent: *
Disallow: /administrator-panel
```

The irony: to hide a page from Google, the developer had to write its exact path in a public file. Navigating to `/administrator-panel` dropped me straight into a fully functional, completely unauthenticated admin panel. I deleted the target user and solved the lab in a couple of minutes.

**Root cause:** The admin panel had zero access control — no login required at all. "Hidden" via convention, not actually protected.

## Lab 2: Unprotected Admin Functionality (Unpredictable URL)

Same idea, harder version — this time `robots.txt` was empty and the URL was a random string, not a guessable word. Wordlist brute-forcing with `ffuf` (a command-line fuzzing tool) came back empty too, since no dictionary would contain a randomly generated path.

The actual answer was hiding in plain sight in the page's HTML source, inside inline JavaScript:

```javascript
var isAdmin = false;
if (isAdmin) {
   var adminPanelTag = document.createElement('a');
   adminPanelTag.setAttribute('href', '/admin-3yrflr');
   adminPanelTag.innerText = 'Admin panel';
   topLinksTag.append(adminPanelTag);
}
```

The key lesson here: `if (isAdmin)` only controls whether the *link gets displayed*. The actual path string, `/admin-3yrflr`, still gets shipped to every single visitor's browser inside the JavaScript file — admin or not. Since I could see the string, I could just navigate there directly, bypassing the intended "check" entirely.

**Root cause:** Sensitive paths shouldn't be baked into client-side code at all, conditional or not — the browser already has the string the moment the page loads.

**Takeaway for future recon:** any time you see JS with `setAttribute('href', ...)`, `fetch(...)`, or similar path assignments — even wrapped inside an `if` — the string inside is worth testing directly, regardless of whether the condition would ever be true for you.

## Lab 3: User Role Controlled by Request Parameter

This one lived in a cookie instead of a URL. Checking DevTools → Application → Cookies showed a cookie literally named `Admin`, set to `false`.

I changed it to `true` in DevTools, refreshed, and navigated to `/admin` — full admin access, no real admin credentials ever involved.

Here's roughly what the vulnerable backend logic looks like:

```javascript
// LOGIN
res.cookie('Admin', user.role === 'admin' ? 'true' : 'false');

// ADMIN CHECK
if (req.cookies.Admin === 'true') {
  return res.render('admin-panel');
}
```

The server computes the role correctly at login — but then hands the *result* of that decision straight to the browser as an editable cookie. Since cookies are just text stored client-side, anyone can open DevTools and rewrite them freely.

**The fix**, and the general pattern for all three labs, looks like this instead:

```javascript
// LOGIN — only an opaque session ID goes to the client
const sessionId = generateSessionId();
sessionStore.save(sessionId, { userId: user.id, role: user.role }); // stored server-side only
res.cookie('session', sessionId, { httpOnly: true });

// ADMIN CHECK — role is looked up server-side, never trusted from the client
const session = sessionStore.get(req.cookies.session);
if (session.role !== 'admin') {
  return res.status(403).send('Forbidden');
}
```

The browser only ever holds a meaningless random session ID. The actual role lives entirely server-side. Even if you tamper with the cookie, there's nothing meaningful to tamper *with*.

## What I'd Tell Someone Starting This Today

1.  **Enumeration comes before exploitation.** Every one of these bugs required actually looking — at `robots.txt`, at page source, at cookies — before there was anything to exploit.
    
2.  **JavaScript doesn't need to be "understood" to be useful for security work.** I don't write JS fluently, but I don't need to — I just need to recognize shapes: `if (role === ...)`, `fetch(...)`, cookie references, hidden path strings. That pattern-matching skill came fast.
    
3.  **It's always the same bug wearing different clothes.** URL in a JS comment, role in a POST body, role in a cookie — structurally identical mistake every time: *the server trusted something the client controls.*
    

Three labs down, a full OWASP Top 10 left to go. Next up: digging into Injection and IDOR properly.

* * *

*This was written as part of my personal learning log while working through the PortSwigger Web Security Academy — all testing was done exclusively against PortSwigger's own intentionally vulnerable lab environments, not live third-party sites.*
