Change One Number. Take Everything.
The #1 Web Vulnerability | Why 94% of Applications Fail This Test

Search for a command to run...
The #1 Web Vulnerability | Why 94% of Applications Fail This Test

The point about authorization being enforced structurally really stood out to me.
The hotel key-card analogy makes IDOR incredibly easy to understand.
Hey, I'm Syed Ahmer Shah. The Engineering Logs is my personal archive of navigating full-stack development, AI integration, and the actual, unglamorized grind of building systems from scratch. Instead of generic tutorials, I write down real architectural decisions, code that broke, and the exact fixes that saved it. Everything here is documented as it happensβfrom optimizing database logic to breaking down complex systems thinking. This is where theory gets thrown out for raw execution. Syed Ahmer Shah | Engineering Logs: Design, Sync, Energize is a transparent look at what it really takes to master the stack and build high-performance architecture. If you are here for clean code, hard technical truths, and zero-bullshit engineering, stick around.
From design-to-code generators to agentic IDEs, here's what's worth your attention β and what's changed since last year.
Why the World's Most Powerful Open AI Model Is Completely Inaccessible: The Kimi K3 Paradox Explained

From Flowstep to v0 β testing which AI tools nail the design, and which ones actually hand you usable code.

No Ghost in the Machine β Just 512 Billion Parameters Doing Linear Algebra at Industrial Scale.

From design-to-code generators to agentic IDEs, here's what's worth your attention β and what's changed since last year.

Syed Ahmer Shah | Engineering Logs: Design, Sync, Energize
38 posts
Hi, I am Syed Ahmer Shah! Welcome to my little corner of the internet. I am a software engineering student, and this is where I document my actual, everyday journey of building full-stack web apps from scratch.
Instead of polished textbooks, you will find my real dev logs, honest lessons from project mistakes, and a good laugh at the bugs along the way.
I dive deep into three main series here: The Engineering Logs, AI vs Reality, and Battle of BackFront.
I write about hands-on coding, testing
You're checking into a hotel. The receptionist hands you a key card and says, "Room 204, third floor." You take the elevator, swipe the card, and walk in.
But what if that key card worked on every room in the building?
What if instead of being locked to Room 204, it opened 205, 206, 301 β any door you tried?
You didn't pick the lock. You didn't break a window. You just tried other doors.
That is IDOR.
And it's the single most widespread vulnerability in web applications today β not because it's sophisticated, but because it's devastatingly easy to miss when you're building fast.
If you've seen a hacking scene in a movie, you know the formula:
A programmer typing furiously in a dark room
Green text cascading down a black screen
Someone saying "I'm in" over dramatic music
The audience gasps
Real-world hacking is almost never like that.
The most damaging breaches in recent years didn't require genius, exotic exploits, or nation-state resources. They required one thing: someone patient enough to change a number in a URL.
Original (yours):
https://app.example.com/api/profile?user_id=101
Modified (someone else's data):
https://app.example.com/api/profile?user_id=102
If the developer didn't check whether you actually own that profile β if the server just pulls the data and returns it β you now have access to someone else's private information.
Their name
Their address
Their private messages
Their payment methods
Whatever that user_id unlocks
This is Insecure Direct Object Reference (IDOR). And it's not theoretical. It's the backbone of some of the most impactful security failures this decade.
The Open Web Application Security Project (OWASP) is a non-profit foundation that tracks and documents the most critical security risks in web applications.
Every few years, they release the OWASP Top 10: a research-backed list of vulnerabilities actively destroying real systems and real user data.
| Year | Rank | Vulnerability |
|---|---|---|
| 2017 | #5 | Broken Access Control |
| 2021 | #1 | Broken Access Control |
What moved to #1? Access control failures. IDOR lives here.
What didn't?
Nation-state malware
AI-assisted intrusions
Zero-day exploits
94% of applications were tested for some form of broken access control, with an average incidence rate of 3.81%.
β OWASP Top 10, 2021
Translation: Security auditors test for Broken Access Control in nearly every application they scan (94% coverage). While only 3.81% of tests trigger a vulnerability, Broken Access Control still holds the #1 spot on the OWASP Top 10. The category maps to 34 separate CWEs (the highest of any group), with IDOR (Insecure Direct Object References) remaining one of the most frequently exploited flaws.
Doesn't require cracking encryption
Doesn't require breaking authentication
Requires curiosity
Requires a willingness to change a parameter
That's the entire attack surface.
A nurse has access to her own assigned patients' files β say, Tab 101.
She logs in
Pulls Tab 101
Does her job
Logs out
Perfect. This is how it should work.
The system is poorly designed. Once logged in, she can type any tab number β 102, 103, 500 β and pull any patient's private medical history from anywhere in the hospital.
She didn't steal credentials. She didn't hack anything. She used her own valid, legitimate access.
The system just forgot to ask one critical question:
"Do you have the right to see this specific record?"
The system authenticated her perfectly β confirmed she was who she said she was.
The system completely forgot to authorize what she could actually touch.
| Concept | Question | Function |
|---|---|---|
| Authentication | "Are you who you say you are?" | Confirms identity |
| Authorization | "Here's what you're allowed to do" | Controls access |
IDOR Happens When: Developers nail authentication but forget authorization entirely.
This is the entire vulnerability.
Here's a simplified API route. You don't need to be a developer to follow the logic β just read it like English.
app.get('/api/orders/:orderId', async (req, res) => {
const orderId = req.params.orderId;
const orders = await db.query(
'SELECT * FROM orders WHERE id = ?', [orderId]
);
res.json(orders); // Returns the order without checking ownership
});
What's wrong here?
The API takes orderId from the URL and returns the result. Full stop.
Missing: A check asking "Does the person making this request actually own this order?"
The Attack:
User logs in as themselves
User requests /api/orders/1001 (their order)
User changes URL to /api/orders/1002 (someone else's order)
Server returns it anyway
Repeat: 1003, 1004, 1005...
Entire orders table exposed (every receipt, address, payment method)
app.get('/api/orders/:orderId', async (req, res) => {
const orderId = req.params.orderId;
const sessionUserId = req.session.userId; // Who's actually logged in?
const orders = await db.query(
'SELECT * FROM orders WHERE id = ? AND user_id = ?',
[orderId, sessionUserId] // Two conditions required
);
if (!orders || orders.length === 0) {
return res.status(403).json({ error: 'Access denied' }); // Deny if not owner
}
res.json(orders[0]); // Return the single matched order
});
What changed?
One line: Get the logged-in user's ID from the session
One condition: Query now requires BOTH order ID AND matching user ID
One check: Return 403 (Forbidden) if the order doesn't belong to this user
The Result:
If someone tries to access Order 1002 while logged in as User 45, but Order 1002 belongs to User 89 β the query returns nothing. Forbidden.
That's the entire fix. That's what prevents a breach that could expose your entire customer database.
Note on HTTP Status Codes: The example uses 403 Forbidden (user exists but lacks access). Some teams prefer 404 Not Found to avoid revealing resource existence. Choose based on your threat model β 403 is more informative for legitimate users, while 404 adds obscurity.
In January 2021, Parler β a social media platform that had grown to 15+ million users β suffered one of the most complete data exposures in recent memory.
| Issue | Impact |
|---|---|
| Publicly accessible API endpoints | Attackers could request any data |
| Sequential, predictable post IDs | Easy to guess and enumerate |
| No rate limiting | Could scrape at scale without detection |
| No authorization validation | APIs returned data to anyone |
Researchers wrote simple scripts that:
Cycled through post IDs (1, 2, 3, 4...)
Retrieved every post ever made on the platform
Including "deleted" posts (still accessible via ID)
Including GPS metadata embedded in media files
Scraped videos, photos, messages in bulk
No server room break-in. No encryption cracked. No sophisticated exploit.
Just predictable IDs + missing authorization checks + no rate limiting = complete platform exposure.
The breach didn't just damage Parler's reputation.
Millions of users had their location data, private messages, and deleted content exposed. The archived data became evidence in federal investigations related to events at the U.S. Capitol.
Real legal consequences. Real harm to millions of users. Driven by a logic gap.
IDOR isn't a failure of intelligence. It's a failure of habit under pressure.
Most developers understand authorization conceptually. Ask them to explain it and they'll give you a solid answer.
But when you're:
40 hours into a sprint
Racing toward a deadline
Writing endpoint after endpoint
Focused on the happy path (user logs in, gets their data, flow works)
The authorization check slips.
You move to the next feature. The adversarial path β what happens when someone deliberately probes your system β rarely makes it onto a sprint board when product launch is three days away.
IDOR is insidiously silent.
A missing authorization check doesn't:
Throw an error
Break tests
Crash staging
Fail CI/CD
A missing authorization check:
Passes your test suite
Works perfectly in staging
Behaves exactly as intended for every legitimate user
Remains completely invisible until exploited
You can't run a linter and catch it. Static analysis tools won't flag it. It's a logic gap.
Logic gaps require deliberate human review, not automated tooling.
This is why OWASP classifies it as critical: it's a problem that tooling can't solve.
Writing an authorization check into each individual endpoint fails the moment a developer forgets once.
Under deadline pressure? It will happen.
Middleware = code that intercepts every request before it hits your endpoint handler, checking permissions upfront.
const requireOwnership = (resourceType, paramName) => async (req, res, next) => {
const resourceId = req.params[paramName];
const userId = req.session.userId;
const isOwner = await checkOwnership(resourceType, resourceId, userId);
if (!isOwner) {
return res.status(403).json({ error: 'Forbidden' });
}
next(); // Only proceed if authorized
};
// Applied to any protected route
app.get('/api/documents/:id',
requireOwnership('document', 'id'), // Permission check first
getDocument // Only execute if allowed
);
Benefits:
One reusable function
Attach to every protected route
Authorization impossible to accidentally omit
Baked into the pattern, not scattered across endpoints
IDOR attacks often involve trying many resource IDs in rapid succession to discover valid ones.
const rateLimit = require('express-rate-limit');
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per IP per window
message: 'Too many requests from this IP'
});
app.use('/api/', apiLimiter);
Why it matters:
Sequential IDs (1001, 1002, 1003...) become trivial to brute-force without rate limiting
A resourceful attacker can scrape thousands of records in seconds
Rate limiting creates friction β makes bulk enumeration time-prohibitive
Three-layer effect:
Layer 1: UUIDs make enumeration hard (can't guess)
Layer 2: Rate limiting makes enumeration slow (requests blocked)
Layer 3: Middleware makes enumeration pointless (requests denied anyway)
Teams at larger scale go further: Row-Level Security (RLS) at the database level.
Here's why this matters:
Application-layer middleware is solid, but it's still software you wrote. One refactor, one mistake, one callback that skips middleware = gap created.
Row-Level Security moves enforcement to the database itself.
| Layer | Who Enforces? | Weakness |
|---|---|---|
| Application Middleware | Your code | Humans forget |
| Database RLS | Database engine | No human variable |
Your database enforces the rule: "For this session user, only return rows where user_id matches this session's userId."
Even if you:
Forget the middleware
Write a raw SQL query
Connect through a different application
Bypass application logic entirely
The database still won't hand over data that doesn't belong to that user.
PostgreSQL (True Native RLS):
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY user_orders ON orders
FOR SELECT USING (user_id = current_user_id());
Oracle (Virtual Private Database): Implemented via VPD context and predicates.
MySQL Limitation: MySQL does NOT have native RLS. To achieve row-level security in MySQL, you must filter at the application layer using middleware (Layer 1 approach), use views with row-level filtering (brittle because views are easily bypassed if application logic changes), or consider PostgreSQL if row-level security is critical for your use case.
Queries execute at the row level in PostgreSQL/Oracle, filtered by the database engine itself before results return. An attacker bypassing your application layer still can't extract data the database layer won't allow.
This is why PostgreSQL with RLS is the gold standard for teams handling sensitive data at scale.
Authorization should be structurally enforced, not manually remembered.
Four-layer defense:
Application Middleware β Primary defense (hard to bypass)
Rate Limiting β Prevents bulk enumeration (limits friction)
Database RLS β Last line of defense (bulletproof, PostgreSQL/Oracle only)
Code Review β Catches logical errors (human layer)
Here's a practicalβand often overlookedβlayer of defense-in-depth: switch from sequential IDs to non-predictable identifiers.
UUIDs are an enhancement that makes attacks harder, not impossible.
This is where teams mess up.
The Parler breach worked because attackers could simply increment post IDs:
/api/posts/1
/api/posts/2
/api/posts/3
/api/posts/4
... (script the rest)
Your API returned results for every number. Bulk scraping was trivial with basic scripting.
/api/posts/550e8400-e29b-41d4-a716-446655440000
/api/posts/6ba7b810-9dad-11d1-80b4-00c04fd430c8
/api/posts/f47ac10b-58cc-4372-a567-0e02b2c3d479
An attacker:
Can't guess the next UUID
Can't script a counter loop
Can't enumerate your entire database with for-loop + curl
But they CAN still exploit the system if authorization checks are missing.
Because IDs alone don't control access.
An attacker who knows a valid UUID but doesn't own it can still request it.
The Trap:
A team:
Switches to UUIDs
Celebrates the "security improvement"
Weakens authorization checks ("now IDs are unpredictable, we're safer")
Ends up with worse security than they started with
It feels safer (harder to guess IDs) but it's actually more vulnerable (no authorization check).
UUIDs solve enumeration. Authorization solves access control. You need both.
Together, they create four layers of defense:
Layer 1: Enumeration Defense
β Attacker can't guess valid resource IDs
β IDs are cryptographically random, not sequential
Layer 2: Access Control
β Attacker can't access IDs they don't own
β Middleware validates ownership before returning data
Layer 3: Rate Limiting
β Attacker can't probe resources at scale
β API rejects rapid successive requests
Layer 4: Database Enforcement
β Even if app-layer middleware fails, RLS blocks unauthorized access
β Database enforces "you can only see your own rows"
| Scenario | Vulnerability | Exploitability |
|---|---|---|
| Sequential IDs + No Auth | Critical | Easy (simple loop) |
| UUIDs + No Auth | Critical | Hard (need valid ID) |
| Sequential IDs + Middleware | Secure | Requires auth bypass |
| UUIDs + Middleware | Very Secure | Requires auth + ID guess |
| UUIDs + Middleware + RLS | Extremely Secure | Requires 3 breaches |
| UUIDs + Middleware + Rate Limit + RLS | Bulletproof | Requires 4 breaches |
Auto-incrementing integers (1001, 1002, 1003):
Designed for usability and storage efficiency
Scream "try the next number" to attackers
Enable trivial bulk scraping
UUIDs (v4):
Designed to be globally unique and unpredictable
Defeat enumeration attacks that make IDOR trivial
Require attackers to know valid IDs first
Together with proper architecture:
Layer 1: UUIDs prevent guessing
Layer 2: Middleware prevents access
Layer 3: Rate limiting prevents bulk enumeration
Layer 4: RLS prevents leakage
This isn't security through obscurityβit's defense-in-depth.
Make the attack surface materially harder to exploit, paired with proper access controls. Use UUIDs alongside, never instead of, proper ownership validation.
You can't wait for external penetration testers to find this. Here's how to catch it before it reaches production.
Steps:
Create two test accounts β User A and User B
Log in as User A, perform an action (create an order, document, post)
Note the resource ID from the URL or API response (e.g., /api/orders/1234)
Copy the full request (using browser dev tools, Postman, or curl)
Log in as User B in a different browser/incognito window
Paste the request, changing only the resource ID to one you know User B doesn't own
If you get access, you have IDOR
Repeat with 5-10 different resources to be thorough
Example with curl:
# User A requests their order (authorized)
curl -H "Cookie: session=user_a_token" https://app.com/api/orders/1001
# Attacker (with User B token) requests User A's order
curl -H "Cookie: session=user_b_token" https://app.com/api/orders/1001
# If this works: IDOR vulnerability confirmed
Add to your Jest/Mocha test suite:
describe('IDOR Protection', () => {
it('should not allow User B to access User A resources', async () => {
// User A creates a document
const userAToken = await login('user_a@test.com', 'password');
const res = await api.post('/documents',
{ title: 'Secret' },
{ headers: { Authorization: `Bearer ${userAToken}` }}
);
const docId = res.data.id;
// User B tries to access it
const userBToken = await login('user_b@test.com', 'password');
const accessAttempt = await api.get(`/documents/${docId}`,
{ headers: { Authorization: `Bearer ${userBToken}` }, validateStatus: () => true }
);
// Should be denied
expect(accessAttempt.status).toBe(403);
});
});
Single-resource access (GET /documents/123)
Batch operations (GET /documents?ids=1,2,3 β can attacker filter others' IDs?)
Nested resources (GET /users/123/posts/456 β is both owner checked?)
Update operations (PUT /documents/123 β can attacker modify others' docs?)
Delete operations (DELETE /documents/123 β can attacker delete others' docs?)
Export/Download (GET /documents/123/export β does permission checking apply here too?)
Common blind spots:
Admin endpoints often skip authorization ("if (req.user.isAdmin)...")
File upload/download endpoints ("just serve the file")
Reporting endpoints ("aggregate data, no need to filter")
Export features ("CSV download, should be open")
Test these especially carefully.
IDOR doesn't stop at single-level access. For hierarchical APIs, check authorization at every level.
GET /users/123/posts/456/comments/789
An attacker might:
Know User 123 exists
Try User 124, 125, 126... to enumerate users
For a user they don't know, try accessing their posts
For a valid post, try changing the comment ID
Each ID in the path needs independent validation:
app.get('/users/:userId/posts/:postId/comments/:commentId',
requireOwnership('user', 'userId'), // User must own the user record
requireOwnership('post', 'postId'), // User must own the post
requireOwnership('comment', 'commentId'), // User must own the comment
getComment
);
Without this chaining, an attacker only needs to guess one ID to cascade access.
The Persistent Belief: "Serious threats require serious technical sophistication."
Attackers running server farms
AI-assisted intrusion kits
Reverse-engineering firmware for weeks
Nation-state-level resources
Reality: Some do this. Most don't.
OWASP's research tells a consistent story: The vulnerabilities doing the most damage are simple.
Changing a number in a URL β Parler
Sending a request your account shouldn't allow
Accessing a file path nobody thought to restrict
Missing an authorization check
HackerOne β the world's largest vulnerability disclosure platform β reports:
IDOR and broken access control consistently represent a major portion of valid, high-severity submissions.
Companies are paying researchers thousands of dollars to find issues that would have taken a developer ten minutes to prevent at build time.
The economics are completely backwards.
And the people bearing the cost aren't just companies. They're users who trusted those companies with their data.
The question isn't whether your application has an IDOR vulnerability.
Given OWASP numbers, probability is not on your side.
The question is: Do you have a process that would catch one before it's exploited?
Ask yourself:
Do you audit your API routes for ownership validation?
Do you test your application like an adversary would?
Not as a logged-in user with good intentions
But as someone acting maliciously, trying to access data you shouldn't allow them to access
Do you enforce authorization at the architecture level?
Are your resource IDs predictable?
Sequential integers invite enumeration
Would UUIDs meaningfully change your attack surface?
Do you have rate limiting in place to prevent bulk enumeration?
For sensitive data, do you use database-level RLS (PostgreSQL)?
You still live with the consequences.
Broken Access Control sitting at #1 on OWASP's list for multiple cycles is not an anomaly.
It's a signal that the industry is consistently prioritizing shipping over securing.
The most dangerous exploits are not the ones requiring genius.
They're the ones requiring a curious person and one moment of developer oversight.
OWASP Top 10 2021 β A01: Broken Access Control
https://owasp.org/Top10/A01\_2021-Broken\_Access\_Control/
OWASP Testing Guide β Insecure Direct Object Reference
https://owasp.org/www-project-web-security-testing-guide/v41/4-Web\_Application\_Security\_Testing/05-Authorization\_Testing/04-Testing\_for\_Insecure\_Direct\_Object\_References
CWE-639 β Authorization Bypass Through User-Controlled Key
https://cwe.mitre.org/data/definitions/639.html
PortSwigger Web Security Academy β IDOR Explained
https://portswigger.net/web-security/access-control/idor
HackerOne Hacker-Powered Security Report
https://www.hackerone.com/resources/reporting/the-2022-hacker-powered-security-report
Want to test your understanding? Try:
PortSwigger's interactive IDOR labs (free)
HackTheBox vulnerable applications
OWASP WebGoat project
For your team:
Add IDOR testing to your security code review checklist
Include authorization validation in your API design guidelines
Implement middleware patterns as defaults, not options
Add automated IDOR tests to your CI/CD pipeline
If using PostgreSQL, enable Row-Level Security on sensitive tables
Priority:
Implement UUID resources (not sequential IDs)
Add reusable authorization middleware on all protected routes
Add basic rate limiting to API endpoints
Why: Fast to implement, covers 95% of IDOR attacks, patterns are portable
Implementation time: 2-3 days
Code template:
// Middleware
const auth = (req, res, next) => {
if (req.user?.id !== req.params.userId) return res.status(403).send('Forbidden');
next();
};
// Route
app.get('/users/:userId/data', auth, handler);
Add to small team setup:
UUID resources β
Middleware patterns β
Rate limiting β
Database-level row filtering (view-based or app-enforced)
Automated IDOR tests in CI/CD
Why: Data complexity growing, multiple code paths to protect
Implementation time: 1-2 weeks
Full implementation:
UUIDs (enumeration defense)
Middleware (access control)
Rate limiting (bulk enumeration prevention)
PostgreSQL with RLS (database enforcement)
Automated testing + penetration testing
Security audit of all endpoints
Why: Eliminates IDOR as realistic attack vector, defense-in-depth, audit-ready
Implementation time: 2-4 weeks
For developers:
"Authorization should be structurally enforced, not manually remembered."
For architects:
"The database enforces the rule. It's physics, not policy."
For security teams:
"The most dangerous exploits are not the ones requiring genius. They're the ones requiring a curious person and one moment of developer oversight."
For managers:
"A developer forgetting one authorization check takes ten minutes to prevent, thousands of dollars to remediate, and immeasurable time to recover reputation."
Portfolio: ahmershah.dev
Crunchbase: @syed-ahmer-shah
Crunchbase Company: @syedahmershah
Clutch: @syed-ahmer-shah
Tech Behemoth: @syed-ahmer-shah
Design Rush: @syed-ahmer-shah
Edverise: @syed-ahmer-shah
Trust Pilot: @ahmershah.dev
LinkedIn: Syed Ahmer Shah
GitHub: @ahmershahdev
AWS Builder Profile: @syedahmershah
DEV: @syedahmershah
Medium: @syedahmershah
Hashnode: @syedahmershah
Substack: @syedahmershah
HackerNoon: @syedahmershah
Substack: @syedahmershah
Facebook: @ahmershahdev
Linkedin Page: @syedahmershah
YouTube: @ahmershahdev
Instagram: @ahmershahdev
TikTok: @ahmershahdev