We closed module 2 by announcing that in module 3 we would walk the Top Ten category by category over BazarNube's real code, turning vocabulary into operational knowledge. We start where the 2021 list itself starts: A01 – Broken Access Control. Its first-place ranking is no accident: it is the most widespread category, the one that affects the most applications, and the one with the most direct impact on the confidentiality and integrity of data.
Access control decides what each authenticated user is allowed to do. When it fails, an ordinary BazarNube customer can read another customer's orders, a user without permissions can reach the admin panel, or anyone can modify resources that don't belong to them. In this course we'll illustrate it with a very typical case: an endpoint in BazarNube's Node/Express API that returns orders using the identifier that arrives in the URL directly, without checking whether that order belongs to the user asking for it.
Legal and ethical notice: every attack technique shown here is illustrative and uses fake data. Practice only on your own systems or with explicit written authorization. Testing access control on third-party systems without permission is a crime in most jurisdictions.
Contents
- Authentication vs. authorization: don't confuse A01 with A07
- IDOR and insecure direct object references
- Horizontal and vertical privilege escalation
- Forced browsing and client-side access control
- Misconfigured CORS as an access-control failure
- Prevention principles: deny by default, authorize on the server
- Authorization models: RBAC and ABAC
- Common mistakes and tips
- Exercises and solutions
- Authentication vs. authorization
These are two distinct, consecutive controls:
| Concept | Question it answers | OWASP category |
|---|---|---|
| Authentication | Who are you? Are you who you claim to be? | A07 (lesson 03-09) |
| Authorization / Access control | Once identified, what are you allowed to do? | A01 (this lesson) |
A system can authenticate perfectly (solid login, MFA, well-managed sessions) and still have broken access control: it knows who you are, but it doesn't check whether you may access the resource you're asking for. A01 deals with this second control.
- IDOR and insecure direct object references
An IDOR (Insecure Direct Object Reference) happens when the application exposes a reference to an internal object (a database id, a file name) and decides access solely from that user-controlled value, without verifying ownership of the resource.
Vulnerable code in BazarNube
Lucía, the backend lead, wrote this endpoint to look up an order's details:
// routes/orders.js (BazarNube Node/Express API) — VULNERABLE
router.get('/api/orders/:orderId', requireLogin, async (req, res) => {
// requireLogin only checks that there is a valid session (authentication)
const order = await db.query(
'SELECT * FROM orders WHERE id = $1',
[req.params.orderId]
);
if (order.rows.length === 0) return res.status(404).json({ error: 'Not found' });
res.json(order.rows[0]); // returns the order no matter who it belongs to
});The requireLogin middleware guarantees there is an active session, but nobody checks that the order belongs to that session's user. The query filters by id, not by owner.
How it's exploited (illustrative)
The customer Ana (user 501) sees her order at GET /api/orders/1042. Since the ids are numeric and sequential, she tries incrementing them:
GET /api/orders/1043 -> another customer's order (name, address, amount) GET /api/orders/1044 -> yet another one
With a simple loop she can download the entire store's order history: personal data, addresses and purchase lines. It's a massive leak with no need to "hack" anything; changing a number is enough.
Fixed code
The rule is: the server must tie every access to the session identity, not to the identifier the client sends.
// routes/orders.js — SECURE
router.get('/api/orders/:orderId', requireLogin, async (req, res) => {
const order = await db.query(
// we ALSO filter by the owner taken from the server-side session
'SELECT * FROM orders WHERE id = $1 AND user_id = $2',
[req.params.orderId, req.session.userId]
);
// Return 404 (not 403) so we don't reveal that the id exists but belongs to someone else
if (order.rows.length === 0) return res.status(404).json({ error: 'Not found' });
res.json(order.rows[0]);
});Key details:
req.session.userIdcomes from the server (from the session), never from a parameter the client can tamper with.- The
AND user_id = $2condition turns the authorization check into part of the query itself: if the order isn't yours, it simply doesn't exist for you. - Responding with
404instead of403avoids leaking the existence of other people's resources (a small reinforcement against enumeration).
As an extra layer, many teams replace sequential ids with UUIDs or unguessable identifiers. Careful, though: this hinders enumeration but does not replace the ownership check. A leaked UUID (in a log, in an email) would still be accessible. The real defense is the server-side check.
- Horizontal and vertical privilege escalation
- Horizontal escalation: you access resources belonging to another user at the same level. The IDOR above is exactly this: Ana reading other customers' orders.
- Vertical escalation: you gain the privileges of a higher role. For example, a customer calling an admin endpoint.
Vertical example in BazarNube
// routes/admin.js — VULNERABLE: only checks login, not the role
router.post('/api/admin/products/:id/price', requireLogin, async (req, res) => {
await db.query('UPDATE products SET price = $1 WHERE id = $2',
[req.body.price, req.params.id]);
res.json({ ok: true });
});Any authenticated customer could change prices. The fix requires checking the role on the server:
// middleware/authorize.js
function requireRole(...roles) {
return (req, res, next) => {
if (!roles.includes(req.session.role)) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
}
// routes/admin.js — SECURE
router.post('/api/admin/products/:id/price',
requireLogin, requireRole('admin'), async (req, res) => { /* ... */ });The role is taken from req.session.role, set at login from the database, never from a field sent by the client (such as a role in the body or in a tamperable cookie).
- Forced browsing and client-side access control
A frequent mistake in SPAs like BazarNube's React front end is to hide admin buttons or routes on the client and believe that protects them. It doesn't: the browser is the attacker's territory.
// AdminLink.jsx — this is UX, NOT security
{user.role === 'admin' && <Link to="/admin">Panel</Link>}Hiding the link improves the experience, but anyone can navigate directly to /admin or, more importantly, call POST /api/admin/... with curl. Every authorization decision must be repeated and enforced on the server. The client may reflect permissions; the server must impose them.
Forced browsing consists precisely of requesting unlinked URLs directly (/api/admin, /backup.zip, /api/internal/metrics) in the hope that they are unprotected. It's prevented by applying the same access control to every endpoint, including those that never appear in the interface.
- Misconfigured CORS
CORS (Cross-Origin Resource Sharing) controls which web origins can read responses from your API in the browser. A lax configuration can turn into an access-control failure.
// VULNERABLE: reflects any origin and also allows credentials
app.use(cors({
origin: (o, cb) => cb(null, true), // accepts ALL origins
credentials: true // ...sending session cookies
}));Reflecting the attacker's Origin together with credentials: true lets a malicious site make authenticated requests to BazarNube's API on the victim's behalf and read the responses. The fix is an explicit allowlist:
// SECURE
const allowed = ['https://bazarnube.com', 'https://app.bazarnube.com'];
app.use(cors({
origin: (o, cb) => cb(null, !o || allowed.includes(o)),
credentials: true
}));Never use origin: '*' combined with credentials (the browser itself blocks it, but reflecting the origin is the dangerous equivalent).
- Prevention principles
flowchart TD
A[Incoming request] --> B{Valid session?}
B -- No --> X[401 Unauthenticated]
B -- Yes --> C{Authorized for this resource?}
C -- No --> Y[403/404 Denied by default]
C -- Yes --> D[Execute action]
The golden rules of A01:
- Deny by default. Every endpoint starts from "forbidden" and only allows what is explicitly authorized. Nothing is left open by oversight.
- Always authorize on the server, with identity taken from the session, not from client parameters.
- Centralize authorization logic in reusable middleware/services, rather than scattering and copying it endpoint by endpoint (where it's easy to forget).
- Check resource ownership on every object access (prevents IDOR).
- Log access-control failures and alert on abuse patterns (links to A09, lesson 03-11).
- Authorization models: RBAC and ABAC
| Model | Idea | Example in BazarNube | When to use it |
|---|---|---|---|
| RBAC (role-based) | Permission depends on the user's role | admin, support, customer |
Stable, role-based rules |
| ABAC (attribute-based) | Permission depends on attributes of user, resource and context | "a seller can only edit products in their store" | Fine-grained, data-dependent rules |
BazarNube combines both: RBAC to separate customer/support/admin, and an ABAC layer for "each seller manages only their own." The IDOR from section 2 is, at its heart, an ABAC rule (order.user_id == session.userId) that was missing.
Common Mistakes and Tips
- Trusting the client. Hiding buttons is not authorizing. Repeat the check on the server, always.
- Using the client's id as the source of identity. Take
userId/rolefrom the server-side session, never from the body, the query, or an unsigned cookie. - Copy-pasted authorization. If every endpoint reimplements the check, one of them will be forgotten. Centralize it in middleware.
- Assuming a UUID protects you. It hinders enumeration, it doesn't authorize. Keep the ownership check.
- Forgetting the "secondary" methods. You protect
GETbut leavePUT/DELETEopen. Cover every verb and every endpoint, including internal ones. - Tip: write automated authorization tests ("user A cannot read B's order") and run them in CI. IDORs are very well caught by tests.
Exercises
Exercise 1. The following BazarNube endpoint lets a user download an invoice. Identify the vulnerability and fix it.
router.get('/api/invoices/:id/pdf', requireLogin, async (req, res) => {
const inv = await db.query('SELECT pdf_path FROM invoices WHERE id = $1',
[req.params.id]);
res.download(inv.rows[0].pdf_path);
});Exercise 2. Marc proposes "fixing" the order IDOR by swapping the numeric ids for UUIDs and leaving the query untouched. Is that enough? Justify your answer.
Exercise 3. Classify each scenario as horizontal or vertical escalation: a) A customer accesses another customer's cart. b) A support user runs an action reserved for administration. c) A seller edits a product from another store.
Solutions
Solution 1. It's an IDOR: any invoice is downloaded by id without checking ownership, and it doesn't even validate that the row exists (it would fail with undefined). Fix:
router.get('/api/invoices/:id/pdf', requireLogin, async (req, res) => {
const inv = await db.query(
'SELECT pdf_path FROM invoices WHERE id = $1 AND user_id = $2',
[req.params.id, req.session.userId]);
if (inv.rows.length === 0) return res.status(404).json({ error: 'Not found' });
res.download(inv.rows[0].pdf_path);
});Solution 2. It's not enough. A UUID only makes ids harder to guess, but if one leaks (email, log, shared browser history) access is still open because there's no ownership check. The correct defense is to add AND user_id = $2. The UUID is a complementary layer, not the primary one.
Solution 3. a) horizontal; b) vertical; c) horizontal (same role level, different resource owner; it's an ABAC rule).
Conclusion
Broken access control tops the Top Ten because it's easy to introduce (just forget a check) and very damaging (leaking or manipulating other people's data). The keys we noted in BazarNube's backlog: deny by default, take identity from the server-side session, check ownership and role on every access, centralize authorization and never trust the client.
Backlog entry — A01: fixed the IDOR in GET /api/orders/:orderId (added the user_id filter), added requireRole('admin') to the admin endpoints and replaced the reflected CORS with an allowlist. Pending: authorization tests in CI.
Once we know that only the right person can reach a piece of data, the next question arises: when that data travels or is stored, is it protected? That leads us to the next lesson, A02:2021 – Cryptographic Failures and Sensitive Data Exposure, where we'll see how BazarNube protects passwords, cards and traffic.
OWASP Course: Guidelines and Standards for Web Application Security
Module 1: Introduction to OWASP
Module 2: Main OWASP Projects
- OWASP Top Ten
- OWASP ASVS (Application Security Verification Standard)
- OWASP SAMM (Software Assurance Maturity Model)
- OWASP ZAP (Zed Attack Proxy)
- Other Key Projects: WSTG, Cheat Sheets and Dependency-Check
Module 3: OWASP Top Ten 2021 in Depth
- A01:2021 – Broken Access Control
- A02:2021 – Cryptographic Failures and Sensitive Data Exposure
- A03:2021 – Injection
- Cross-Site Scripting (XSS) in Depth
- A04:2021 – Insecure Design
- A05:2021 – Security Misconfiguration
- XML External Entities (XXE)
- A06:2021 – Vulnerable and Outdated Components
- A07:2021 – Identification and Authentication Failures
- A08:2021 – Software and Data Integrity Failures (Insecure Deserialization)
- A09:2021 – Security Logging and Monitoring Failures
- A10:2021 – Server-Side Request Forgery (SSRF)
Module 4: OWASP ASVS (Application Security Verification Standard)
Module 5: OWASP SAMM (Software Assurance Maturity Model)
Module 6: OWASP ZAP (Zed Attack Proxy)
- Introduction to ZAP
- Installation and Configuration
- Vulnerability Scanning
- Automating Security Testing
Module 7: Best Practices and Recommendations
- Secure Software Development Life Cycle (SDLC)
- Threat Modeling
- Integrating Security into DevOps (DevSecOps)
- Security Training and Awareness
- Additional Tools and Resources
Module 8: Practical Exercises and Case Studies
- Exercise 1: Identifying Vulnerabilities
- Exercise 2: Implementing Security Controls
- Case Study 1: Analyzing a Security Incident
- Case Study 2: Improving the Security of a Web Application
