Difficulty: 🟢 Apprentice

Goal:

  • 💬 Go to Live chat — send any message — click "View transcript" — observe the download URL contains an incrementing filename like 2.txt
  • ✏️ Change the filename to 1.txt — the server serves carlos's earlier transcript directly from the filesystem — no ownership check
  • 🔑 Find the password carlos typed in that chat → log in as carlos → lab solved!

🧠 Concept Recap

Insecure direct object references on static files is an IDOR where the "object" is not a database record but a raw file on disk. Chat transcripts are saved as numbered text files (1.txt, 2.txt, ...) and served via a direct static URL. The server applies no access control at all — any user can request any filename. This is IDOR at its most literal: the reference (1.txt) directly maps to an object (a file on the filesystem), and it's insecure because the server never checks whether the requester owns that file. The unique twist here is that carlos typed his own password into the live chat — and that conversation was saved verbatim.

📊 Lab 5 vs Lab 6 — Two IDOR Flavours

Key twist in this lab — two vulnerabilities combined:
  1. IDOR: any user can access any transcript by changing the filename
  2. Sensitive data in transcript: carlos typed his password into the chat
     → The system stored it verbatim in a plain text file on disk

Neither vulnerability alone is as critical:
  → IDOR with harmless chat content = low severity
  → Password stored in file but no IDOR = medium severity
  → Both together = full account takeover via password disclosure

The full attack flow:

Live chat → send a message → View transcript
         ↓
Browser downloads: /download-transcript/2.txt
  → File contains your own chat session
         ↓
Change URL: /download-transcript/2.txt → /download-transcript/1.txt
  → Server opens 1.txt from disk — no auth check — no ownership check
  → File contains carlos's earlier chat:

CONNECTED: -- Now chatting with Hal Chatbot --
    CARLOS: I forgot my password
    HAL: No problem - Please enter your new password.
    CARLOS: mypassword123
    HAL: Thank you - your password has been reset.
         ↓
Log in as carlos with the discovered password → lab solved ✅

🛠️ Step-by-Step Attack

🔧 Step 1 — Use the Live Chat Feature

  1. 🌐 Click "Access the lab"
  2. 🖱️ Click the "Live chat" tab at the top of the page
  3. 🖱️ Type any message (e.g. hello) → click "Send"
  4. 🖱️ Click "View transcript"
What just happened:
  → The browser downloads a .txt file — your chat transcript
  → This triggers a GET request to something like:
      GET /download-transcript/2.txt
  → The file is named with an incrementing integer
  → Your session was the second one → you got 2.txt
  → Carlos's session (where he reset his password) was the first → 1.txt

Why send a message first?
  → "View transcript" only appears after at least one message is sent
  → The transcript download also reveals the URL pattern we need to exploit

🔧 Step 2 — Observe the Download URL

  1. 🖱️ In Burp → Proxy → HTTP history → find GET /download-transcript/2.txt
GET /download-transcript/2.txt HTTP/1.1
Host: YOUR-LAB-ID.web-security-academy.net
Cookie: session=...
Critical observations:
  → The filename is a plain integer with .txt extension: 2.txt
  → Incrementing integers are trivially enumerable: 1, 2, 3, 4...
  → The server is serving a static file directly from disk
  → There is no ownership token, no session check tied to the filename
  → Just knowing or guessing a filename gives you that file

This is IDOR on a static file:
  → Object      = the .txt file on the server's filesystem
  → Reference   = the filename (1.txt, 2.txt, ...)
  → Insecure    = no check that requester owns this transcript
  → Direct      = filename maps 1-to-1 to a file path on disk

🔧 Step 3 — Request 1.txt

Option A — Browser (simplest, no Burp needed):

  1. 🖱️ In your address bar, navigate directly to:
https://YOUR-LAB-ID.web-security-academy.net/download-transcript/1.txt

Option B — Burp Repeater:

  1. 🖱️ Right-click GET /download-transcript/2.txt"Send to Repeater"
  2. 🖱️ Change 2.txt1.txt in the request line → "Send"
GET /download-transcript/1.txt HTTP/1.1
Host: YOUR-LAB-ID.web-security-academy.net
Cookie: session=...
Expected result:
  → Server returns 200 OK with the contents of 1.txt
  → No auth error, no 403, no "this file doesn't belong to you"
  → The static file server (or the app route) simply opens and returns the file

🔧 Step 4 — Read Carlos's Password from the Transcript

  1. 🖱️ Read the response — you'll see a chatbot conversation:
CONNECTED: -- Now chatting with Hal Chatbot --
CARLOS: I forgot my password
HAL: No problem - Please enter your new password.
CARLOS: [carlos's password here]
HAL: Thank you - your password has been reset.
DISCONNECTED
What to copy:
  → The line where CARLOS responds to "Please enter your new password"
  → That value IS his current password (the reset was completed)
  → Copy only the password — not the "CARLOS: " prefix

Why is this in the transcript at all?
  → The chatbot asked carlos to type his new password into the chat window
  → The transcript logs every message verbatim - including the password
  → This is a design flaw: password input should never go through a chat interface
  → The chat system had no idea "this message is a password - don't log it"

2. 🖱️ Copy carlos's password

🔧 Step 5 — Log In as Carlos

  1. 🖱️ Navigate to "My account" → log in with:
  • Username: carlos
  • Password: (what you found in 1.txt)
Result:
  → Logged in as carlos ✅
  → Lab solved! 🎉

🎉 Lab Solved!

✅ Used Live chat → View transcript → observed filename 2.txt
✅ Changed URL to 1.txt → server returned carlos's transcript with no auth check
✅ Found carlos's password in the chat log
✅ Logged in as carlos
✅ Lab complete!

🔗 Complete Attack Chain

1. Live chat → send any message → View transcript
   → Observe: GET /download-transcript/2.txt

2. Change 2.txt → 1.txt (browser URL bar or Burp Repeater)
   → Server serves file with no ownership check

3. Read 1.txt → find carlos's password typed in the chatbot session

4. Log in as carlos → lab solved

No Burp required. Minimal steps. The attack is just changing one number in a URL.

🧠 Deep Understanding

Why is this IDOR on a "static file"?

Classic IDOR (Lab 5): attacker manipulates an ID → DB query returns wrong record
  → ?id=carlos → SELECT * FROM users WHERE username='carlos'

This lab: attacker manipulates a filename → server opens wrong file
  → /1.txt → open('/var/app/transcripts/1.txt')

The access control failure is identical - just different storage layers:
  → DB-backed IDOR:   missing WHERE owner_session = current_session check
  → File-backed IDOR: missing "does this file belong to the current session?" check

Static file servers (nginx, Apache serving a directory) have no concept of file ownership.
If the server is told to serve /transcripts/1.txt and it exists - it will.
Ownership enforcement must come from the application layer, never the file server.

The two compounding vulnerabilities

Vulnerability 1 — IDOR (access control failure):
  → Any user can download any transcript by guessing the filename
  → Severity alone: medium (other users' chat content exposed)
  → Fix: ownership check before serving the file

Vulnerability 2 - Sensitive data logged verbatim (design failure):
  → Carlos typed his password into a chat that records everything
  → The chatbot design caused a password to be stored in a readable log
  → Severity alone: medium (password in a file, but file not yet exposed)
  → Fix: never collect passwords via chat; scrub sensitive patterns before storing

Combined severity: CRITICAL
  → IDOR makes the file readable by anyone
  → The file contains a plaintext password
  → Result: full account takeover with zero effort

Defence in depth means fixing BOTH:
  → Fix IDOR alone: transcript still contains password - server breach exposes it
  → Fix logging alone: no sensitive data in transcript but it's still world-readable
  → Fix both: layered security with no single point of failure

Incrementing IDs — the enumeration problem

Filenames 1.txt, 2.txt, 3.txt... are trivially predictable:
  → An attacker can automate: for i in range(1, 10000): download(f"{i}.txt")
  → With Burp Intruder: Numbers payload 1–1000, sequential → harvest all transcripts in minutes
  → No brute-force effort — the entire space is small and contiguous

UUIDs reduce this risk:
  → /download-transcript/3f2a8b1c-4d5e-6789-abcd-ef0123456789.txt
  → Cannot enumerate - 2^128 possible values
  → BUT: if there's still no ownership check, knowing a UUID = full access
  → UUIDs solve enumeration; they do NOT solve access control

The only real fix is always the server-side ownership check.
UUIDs are a useful secondary hardening measure, never the primary defence.

No login required — the most severe form

Lab 5: required a valid session (horizontal escalation between authenticated users)
  → Anonymous user → 302 redirect to /login

This lab: /download-transcript/ serves files with no auth whatsoever
  → No session cookie needed
  → curl with no cookies works:
      curl https://LAB-ID.web-security-academy.net/download-transcript/1.txt

Severity levels (worst to best):
  1. ❌ Unauthenticated IDOR (this lab) - anyone on the internet
  2. ❌ Authenticated IDOR (Lab 5)      - any logged-in user
  3. ✅ Properly authorised             - only the owner

🐛 Troubleshooting

Problem: 1.txt returns "Not found" or an empty file
→ Carlos's session is always assigned transcript 1 — it should exist from lab setup
→ Double-check the exact URL path: /download-transcript/1.txt (not /download/1.txt)
→ If in Burp Repeater: verify Host header matches the lab domain exactly

Problem: 1.txt downloads but contains your own chat, not carlos's
→ This shouldn't happen - your session was assigned 2.txt
→ If you sent multiple messages across sessions, try 3.txt, 4.txt, etc.
→ Look for the line "CARLOS:" - that confirms you have the right transcript

Problem: The password from 1.txt doesn't work for carlos's login
→ Copy only the password value - not "CARLOS: " prefix or trailing newline
→ Passwords are case-sensitive - copy exactly as written in the transcript
→ If the lab was reset mid-session, carlos's password may have changed - re-fetch 1.txt

Problem: Can't find the "View transcript" button
→ You must send at least one message first - button appears after sending
→ Type anything in the message box and click "Send", then look below the chat window

💬 In One Line

📄 Chat transcripts were saved as 1.txt, 2.txt, ... and served directly from disk with no ownership check — so requesting 1.txt gave up carlos's chat session where he'd typed his own password. That's IDOR on static files — a direct filename reference with zero authorisation, compounded by sensitive data stored verbatim in plain text.

🔒 How to Fix It

// Priority 1 — Serve transcripts through the application, not as static files
// Add session ownership check before returning the file
// ❌ BAD - nginx/Apache serving directory directly with no auth:
// location /download-transcript/ {
//     root /var/app/transcripts;
// }
// ✅ GOOD - application route with ownership enforcement:

app.get('/download-transcript/:filename', requireLogin, (req, res) => {
  const requestedFile = req.params.filename;
  const sessionId = req.session.id;
  // Check that this transcript was created by the current session
  const ownerSessionId = db.getTranscriptOwner(requestedFile);
  if (ownerSessionId !== sessionId) {
    return res.status(403).send('Forbidden');
  }
  const filePath = path.join('/var/app/transcripts', requestedFile);
  res.sendFile(filePath);
});
// Priority 2 — Use unguessable filenames (defence in depth)
// Replace sequential integers with UUIDs scoped to the session

const { v4: uuidv4 } = require('uuid');
function createTranscript(sessionId, chatHistory) {
  const filename = `${uuidv4()}.txt`;     // not guessable
  saveFile(`/var/app/transcripts/${filename}`, chatHistory);
  db.saveTranscriptOwnership(filename, sessionId);  // ownership record
  return filename;
}

// Now two barriers: can't guess the filename AND ownership check blocks unauthorised access
// Priority 3 — Never collect passwords via chat interfaces
// Chat systems log everything — password input must go through dedicated secure forms
// ❌ BAD - chatbot asks user to type password into chat:
// BOT: "Please enter your new password:"
// USER: "mypassword123"  ← logged verbatim in transcript
// ✅ GOOD - chatbot sends a reset link instead:
// BOT: "I've sent a password reset link to your email. Please use that link to set a new password."
// → Password is set via a dedicated HTTPS form, never through the chat
// → Transcript contains no sensitive data even if IDOR exists
// Priority 4 — Scrub sensitive patterns before writing to any log or transcript
// Last line of defence if design can't be changed immediately

function sanitizeMessage(message) {
  const sensitivePatterns = [
    /\b(?:password|passwd|secret|token|pin|cvv)\b/i,
    /\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b/,  // card numbers
    /\b\d{3,4}\b/  // short PINs (context-dependent)
  ];
  for (const pattern of sensitivePatterns) {
    if (pattern.test(message)) {
      return '[REDACTED - possible sensitive content]';
    }
  }
  return message;
}

👏 If this helped you — clap it up (you can clap up to 50 times!)

🔔 Follow for more writeups — dropping soon

🔗 Share with your pentest team

💬 Drop a comment