$ cat owasp/a01.md
A01:2025
Broken Access Control
The app checks who you are, but forgets to check what you are allowed to touch.
## 1 · why it happens
Authentication answers "who are you?". Access control answers "is this yours?". Developers usually get the first one right because the framework does it. The second one is business logic - and it has to be written for every endpoint, by hand.
The classic case is IDOR: the API takes an ID from the URL and returns that record without checking the owner. In 2025 OWASP also moved SSRF here - the server fetching any URL the user gives it is another way of reaching things you should not.
# JavaScript
// GET /api/invoices/1002
app.get('/api/invoices/:id', requireLogin, async (req, res) => {
const invoice = await db.invoices.findById(req.params.id);
res.json(invoice); // any logged-in user can read any invoice
});app.get('/api/invoices/:id', requireLogin, async (req, res) => {
const invoice = await db.invoices.findOne({
id: req.params.id,
ownerId: req.user.id, // scope every query to the current user
});
if (!invoice) return res.sendStatus(404);
res.json(invoice);
});# C#
[Authorize]
[HttpGet("api/invoices/{id}")]
public async Task<IActionResult> Get(int id)
{
var invoice = await _db.Invoices.FindAsync(id);
return Ok(invoice); // no ownership check
}[Authorize]
[HttpGet("api/invoices/{id}")]
public async Task<IActionResult> Get(int id)
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var invoice = await _db.Invoices
.SingleOrDefaultAsync(i => i.Id == id && i.OwnerId == userId);
return invoice is null ? NotFound() : Ok(invoice);
}## 2 · how attackers find it from outside
| public source | what it tells them |
|---|---|
| Wayback Machine | Old API routes like /api/v1/users/123 that may still work without the checks added in v2. |
| JavaScript bundles & source maps | Every route the frontend calls - including admin routes that are only "hidden" in the UI. |
| Public API docs / Swagger | Which endpoints take IDs, so the attacker knows exactly what to increment. |
→ all OSINT sources · run them on yourself
## 3 · how it's abused
⚠ Recon your own assets, or targets you have written permission to test. Passive OSINT reads public data, but using it to target someone else's systems can still be illegal or against their terms. Examples on this site use example.com placeholders.
GET /api/invoices/1002IDOR: the attacker owns invoice 1001 and simply asks for the next number.
../../../etc/passwdPath traversal: climbs out of the upload/download folder to read system files.
..%2f..%2f..%2fetc%2fpasswdSame thing URL-encoded, to slip past a filter that only blocks the literal "../".
http://169.254.169.254/latest/meta-data/SSRF: makes your server call the cloud metadata service and leak its own credentials.
Origin: https://evil.exampleCORS: if the response reflects this origin with credentials allowed, any site can read your users' data.
Payload examples adapted from PayloadsAllTheThings (MIT, © Swissky). Full lists:Insecure Direct Object References · Directory Traversal · Server Side Request Forgery · CORS Misconfiguration · Cross-Site Request Forgery
## 4 · fix
- Deny by default. Every endpoint must decide access explicitly.
- Scope queries to the current user (or tenant) instead of loading by ID and checking later.
- Use random IDs (UUIDs) - not as the fix, but so IDs are not guessable.
- For URL fetching: allow-list hosts, block private and metadata IP ranges.
- Never reflect the Origin header blindly in CORS when credentials are allowed.
## 5 · detect
ZAP
Open-source web app scanner and proxy. The baseline scan is safe for CI.
authorized targets onlyfix at sourceBearer
Static analysis that follows sensitive data through your code.
osint / reconpassiveWayback Machine (CDX API)
Every URL of your site the Internet Archive has seen - including the ones you forgot.
Official description: OWASP A01:2025 Broken Access Control