hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Web Security Fundamentals with @hectoday/http

The Attacker's Mindset

  • Thinking Like an Attacker
  • Project Setup

Injection Attacks

  • SQL Injection
  • SQL Injection: Beyond the Basics
  • Command Injection
  • Header Injection

Cross-Site Scripting (XSS)

  • What Is XSS?
  • Output Encoding
  • Content Security Policy in Practice

Broken Access and Redirects

  • Insecure Direct Object References (IDOR)
  • Open Redirects
  • Server-Side Request Forgery (SSRF)

File and Data Handling

  • Path Traversal
  • Mass Assignment
  • Denial of Service via Input

Putting It All Together

  • Security Testing
  • The OWASP Top 10
  • Capstone: Hardened Notes API

Insecure Direct Object References (IDOR)

The simplest vulnerability to understand and the easiest to miss

The injection and XSS sections covered attacks where the attacker crafts special payloads — SQL code, shell commands, JavaScript. The defenses were about encoding or separating code from data. This section is different. The attacks here require no special characters and no injected code. The attacker just changes ordinary values in the request.

The vulnerable code

Look at the note detail route from the project setup:

route.get("/notes/:id", {
  resolve: (c) => {
    const user = authenticate(c.request);
    if (user instanceof Response) return user;

    const note = db.prepare("SELECT * FROM notes WHERE id = ?").get(c.params.id);
    if (!note) return Response.json({ error: "Not found" }, { status: 404 });

    return Response.json(note);
  },
});

The route authenticates the user (checks they are logged in) but does not check that the note belongs to them. It finds the note by ID, regardless of who owns it.

The attack

Alice is logged in. She views her note: GET /notes/note-1. Then she changes the URL to GET /notes/note-3 — the admin’s note. The server returns it.

# Alice views the admin's note
curl -b cookies.txt http://localhost:3000/notes/note-3
# Returns: { "id": "note-3", "user_id": "admin-1", "title": "Admin Notes", "body": "Secret admin content" }

Alice sees the admin’s secret content. No injection, no scripts, no special payload. Just a different ID.

This is an insecure direct object reference: the application uses a user-supplied reference (the note ID) to access an object directly, without checking authorization.

Why it happens

Developers often think authentication equals authorization:

  • Authentication: Who are you? (Alice, verified by session cookie)
  • Authorization: What can you do? (Alice can access her own notes, not the admin’s)

The route checks authentication (is the user logged in?) but skips authorization (does this user own this note?).

The fix: always check ownership

Add a user_id check to the query:

route.get("/notes/:id", {
  resolve: (c) => {
    const user = authenticate(c.request);
    if (user instanceof Response) return user;

    // SAFE: includes user_id in the WHERE clause
    const note = db
      .prepare("SELECT * FROM notes WHERE id = ? AND user_id = ?")
      .get(c.params.id, user.id);

    if (!note) return Response.json({ error: "Not found" }, { status: 404 });

    return Response.json(note);
  },
});

Now GET /notes/note-3 as Alice returns 404 because note-3 belongs to admin-1, not user-1. The query finds no matching row.

Notice we return 404 (“Not found”), not 403 (“Forbidden”). Returning 403 would tell the attacker that the note exists but they cannot access it. Returning 404 tells them nothing — the note might not exist, or it might belong to someone else. They cannot tell the difference.

Where IDOR hides

IDOR appears in any route that takes an ID from the request and uses it to look up data:

  • GET /notes/:id — reading a note
  • PUT /notes/:id — updating a note
  • DELETE /notes/:id — deleting a note
  • GET /users/:id — viewing a profile
  • GET /invoices/:id — downloading an invoice

Every one of these needs an ownership check. The pattern is always the same: include the authenticated user’s ID in the query.

Exercises

Exercise 1: Fix the GET /notes/:id route. As Alice, try accessing note-3. Verify you get 404.

Exercise 2: Check the POST /notes route. The mass assignment vulnerability (which we fix in Section 5) also has IDOR implications: the attacker can set user_id to create a note owned by another user. Fixing mass assignment fixes this too.

Exercise 3: Write a DELETE /notes/:id route with proper ownership checking. Verify that Alice can delete her own notes but not the admin’s.

Why does the fix return 404 instead of 403 when the note belongs to another user?

← Content Security Policy in Practice Open Redirects →

© 2026 hectoday. All rights reserved.