Skip to main content
Type System Ignored, IDOR ShippedIncident
5 min readFor Security Engineers

Type System Ignored, IDOR Shipped

A development team deployed a customer dashboard that let authenticated users view any account by changing a URL parameter. The vulnerability sat in production for three months before a penetration test caught it. The fix took 20 minutes. The root cause? No one checked authorization, and nothing in the toolchain forced them to.

This isn't about a specific breach. It's a pattern seen in every pentest report, bug bounty program, and incident retrospective. Insecure Direct Object Reference (IDOR) vulnerabilities persist because our tools let developers forget to check authorization. Type-level security offers a way to make that forgetting impossible.

What Happened

The application used a Python Flask backend with standard authentication middleware. When a user logged in, the system verified their credentials and created a session. Routes checked that a valid session existed before serving data.

The problem: authentication isn't authorization. The /api/customer/<customer_id> endpoint verified that someone was logged in, but never checked if that someone should access that specific customer's data. Change the ID in the URL, get someone else's records.

The team caught it during a scheduled penetration test. The tester logged in with a demo account, incremented the customer ID in the browser, and accessed administrative accounts. Impact: full read access to all customer data, including payment methods and transaction history.

Timeline

Month 1: Feature deployed to production after passing unit tests and code review. Tests verified authentication worked. No one wrote tests for authorization boundaries.

Month 2: Normal operation. No alerts, no customer complaints. The vulnerability was invisible to monitoring because successful API calls looked identical to legitimate requests.

Month 3: Penetration test identified the IDOR within the first hour of testing authenticated endpoints. Development team deployed a fix the same day by adding a database query to verify the requesting user owned the requested resource.

Which Controls Failed

The authentication middleware did its job. The failure happened at three levels:

Code review didn't catch it because the reviewer saw authentication checks and assumed authorization was handled. The pattern looked secure at a glance.

Testing didn't catch it because the team wrote tests that verified authenticated users could access their own data. No one wrote tests that verified authenticated users couldn't access other users' data.

Static analysis didn't catch it because standard SAST tools look for SQL injection, XSS, and credential leaks. They don't understand your authorization model. A tool can't flag missing authorization checks unless you've taught it what correct authorization looks like in your codebase.

The real control that failed: nothing in the development toolchain required the developer to think about authorization. The code compiled. The tests passed. The deployment succeeded.

What Standards Require

OWASP ASVS v4.0.3 Requirement 4.1.2 states: "Verify that the application enforces access control rules on a trusted service layer, especially if client-side access control is present and could be bypassed."

Requirement 4.2.1 adds: "Verify that sensitive data and APIs are protected against Insecure Direct Object Reference (IDOR) attacks targeting creation, reading, updating and deletion of records."

PCI DSS v4.0.1 Requirement 6.2.4 requires: "Bespoke and custom software are developed securely" with controls that include "Prevention of common coding vulnerabilities during software-development processes."

ISO 27001 Control 8.3 demands: "Access to information and other associated assets shall be restricted in accordance with the established access control policy."

None of these requirements tell you how to enforce authorization. They tell you it must happen. The gap between "must enforce authorization" and "actually enforcing authorization in every endpoint" is where IDOR lives.

Lessons and Action Items

Lesson one: Authentication and authorization are different problems requiring different controls. Your middleware can verify identity without verifying permission.

Action: Map every endpoint that accepts a resource identifier. For each one, document which user attributes determine access. If you can't articulate the rule, you can't enforce it.

Lesson two: Type systems can encode authorization rules that code review and testing miss. Rust has effectively eliminated most memory corruption vulnerabilities at compile time by making unsafe operations explicit. The Trusted Types web API mitigates most forms of DOM XSS by requiring explicit sanitization at the type level.

Action: If you're using Python, create wrapper types for sensitive identifiers. Instead of accepting customer_id: int, accept customer_id: AuthorizedCustomerId. The constructor for AuthorizedCustomerId forces the authorization check. Now the type system prevents you from passing an unverified ID to a sensitive function.

Here's what that looks like in practice:

class AuthorizedCustomerId:
    def __init__(self, customer_id: int, requesting_user: User):
        if not self._user_owns_customer(customer_id, requesting_user):
            raise AuthorizationError
        self.value = customer_id

Your endpoint becomes:

@app.route('/api/customer/<int:customer_id>')
def get_customer(customer_id: int):
    authorized_id = AuthorizedCustomerId(customer_id, current_user)
    return fetch_customer(authorized_id)

Now fetch_customer only accepts AuthorizedCustomerId, not raw integers. You can't forget the check; the code won't compile (or run, in Python's case) without it.

Lesson three: This approach has costs. Wrapper types add boilerplate. Your team needs to understand why AuthorizedCustomerId exists and use it consistently. If someone adds a new endpoint and bypasses the wrapper "just this once," you're back where you started.

Action: If wrapper types feel too heavyweight, write a custom linter rule that flags any function accepting resource IDs without authorization checks. The linter won't be as bulletproof as the type system, but it catches the pattern during CI before code review.

Lesson four: AI-generated code makes this more urgent. When you ask an LLM to "add an endpoint to fetch customer data," it'll give you authentication because that's in every example. It won't give you authorization because authorization logic is specific to your business rules.

Action: If you're using AI coding assistants, include authorization patterns in your prompts. Better yet, use wrapper types so the AI has to work with AuthorizedCustomerId from the start. The type signature becomes documentation that both humans and models can't ignore.

The team that shipped this IDOR fixed it in 20 minutes once they knew about it. The problem wasn't knowledge. It was that nothing in their process forced them to apply that knowledge at the right moment. Type-level security moves the enforcement from code review and testing, where humans can forget, to the compiler or runtime, where forgetting isn't an option.

Topics:Incident

You Might Also Like