---
title: "How I Found IDORs That Shouldn’t Exist"
description: "IDORs that survive modern access control: trailing slashes, path normalisation, double slashes and obfuscated paths, with testable examples."
canonical_url: "https://amrelsagaei.com/how-i-found-idors-that-shouldnt-exist"
last_updated: "2025-10-26"
---

<post-media video="lfFfJTEFK4Y">



</post-media>

Imagine you’re testing a major program. You poke one endpoint and, suddenly, you can see customer data you absolutely shouldn’t. Not because of a flashy payload - because of sloppy access control logic.

That’s IDOR territory. Not the classroom version, but real-world IDORs that lead to account takeover, data exposure, or worse. This article skips definitions and goes straight to the tools and tricks that find impact in live targets.

## IDORs are everywhere

CRMs, e-commerce, project trackers, BI dashboards, healthcare portals - any app built around users, objects, or lists can contain IDORs. The question isn’t *if* they exist - it’s *where* they hide. That means your hunting starts with mapping the app’s data surface (APIs, list endpoints, profile pages), not blind ID flipping.

## How to think about it

Novice approach: change `id=10` to `id=9` and hope for a popup.<br />


Pro approach: ask what the endpoint assumes, how it’s parsed, and how different layers (router, proxy, app, cache) normalize requests.

Shift your question from “Can I change this number?” to:

- Which controller handles this path?
- Is there a separate endpoint that bypasses the same check?
- Does the app accept alternate formats (strings, encoded chars, different versions)?

That mental model is the difference between a noisy scan and a real find.

## Example context & assumptions

- Assume your user ID = `100`.
- Endpoint we see:```text
/api/v5/users/100
```
- For another user `99` you get `403` - normal. But that `403` is the starting point, not the end.

Below are the practical techniques I run, in order (from cheap/noise to the deeper ones).

## The techniques (what to try - with examples)

> **Rule:** keep tests low-noise, log every request/response pair (status + path) and redact values you can’t show publicly.

### 1) Trailing slash / path normalization

Some auth checks run on normalized paths but others don’t.

```text
/api/v5/users/100       → 200
/api/v5/users/99        → 403
/api/v5/users/99/       → 200
```

Try adding/removing trailing slashes.

### 2) Double-slash or obfuscated path

Routers and proxies sometimes collapse or ignore duplicate separators.

```text
/api/v5/users/99        → 403
/api/v5//users//99      → 200
```

Double slashes, `/.`, `//.` variations - quick to test.

### 3) Version downgrade (legacy endpoints)

Older API versions may miss modern checks.

```text
/api/v5/users/99        → 403
/api/v2/users/99        → 200
```

Try `v4`, `v3`, `v2` - especially on apps that keep multiple API versions.

### 4) Subpath / endpoint variant (profile / details)

Different controllers sometimes apply inconsistent rules.

```text
/api/v5/users/99/profile    → 200
/api/v5/users/99/details    → 200
```

Check `/profile`, `/details`, `/info`, `/summary` variants.

### 5) Multi-ID / filter parameter abuse

List endpoints that accept filters or multi-IDs can leak others.

```text
/api/v5/users?where=id:100,99   → returns both records
```

Try comma lists, `where` filters, `ids[]=…`, etc.

### 6) Query param vs path

Same resource accessible through different routes with different checks.

```text
/api/v5/users/99           → 403
/api/v5/users?id=99        → 200
```

Test `id` in path vs query string.

### 7) Type confusion - string vs integer

Different parsing code paths may treat types differently.

```text
/api/v5/users/99           → 403
/api/v5/users/"99"         → 200
/api/v5/users/abc99        → 200
```

Try quoting numbers, adding alpha prefixes, or sending JSON body IDs instead.

### 8) Leading zeros / hex / alternate formats

Numeric parsing quirks can be abused.

```text
/api/v5/projects/099       → 200
/api/v5/projects/0x63      → 200
```

Test `099`, `0xNN`, `0NNN` forms where applicable.

### 9) NULL / termination / control characters (`%00`)

Some parsers don’t expect control chars and end up truncating or ignoring trailing validation.

```text
/api/v5/projects/99%00     → 200
```

Null byte (`%00`) and other control encodings can bypass checks when not properly sanitized.

### 10) Header / proxy-based bypass (X-Original-URL / X-Forwarded-For)

Reverse proxies sometimes forward headers that alter internal routing or auth decisions. Test carefully and within program rules.

```http
GET /api/v5/admin/99
Host: target
X-Original-URL: /api/v5/admin/100
→ returns 200 (if proxy honored header)
```

**Caution:** header tests can be noisy; know program policy and keep it passive.

### 11) **Unicode / encoded-space trick - the deep/winner technique**

This is the one that repeatedly gave me real, high-impact bugs. It’s simple: introduce an encoded space (`%20`) or subtle encoded character around the ID so different layers normalize it differently.

```text
/api/v1/projects/99       → 403
/api/v1/projects/99%20    → 200   ← the hit
```

Variants to try: `%20` before the ID, trailing `%20`, `%20/`, `%09`, combined with version downgrade or trailing slashes.

This trick works because one layer sees `99%20` as `99`(with a trailing space) and another layer strips/normalizes it differently - effectively sidestepping the auth hook.

### 12) Composite / chained variants

If a single trick fails, combine two or three:

```text
/api/v2/projects/99%20/   → 200
```

Downgrade + encoded char + path tweak = often effective.

## Quick recap - checklist to copy in your notes

- Find the JSON/API endpoints (not the rendered HTML) - that’s your surface.
- Try cheap, low-noise tweaks first: trailing slash, double slash, subpaths, query params.
- Test version downgrades - old APIs are gold.
- Try type/format tricks: strings, leading zeros, hex.
- Try encoding tricks: `%00`, `%20`, control chars.
- Combine tricks when single tests fail.
- Log request + response (status + body snippet) - that becomes your PoC.

## Outro

This is tactical work: small tweaks, methodical testing, and the right mindset. A boring `403` isn’t an obstacle - it’s a diagnosis. Ask the right questions, test the right angles, and you’ll stop getting noise and start finding impact.

If you want more tactical breakdowns (short, focused, practical), tell me which area: API logic, CORS chains, file uploads, or something else - and I’ll dig one up. I read every comment.

If you have questions, leave them below or ping me on Twitter.

## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
