Copypasta: IDOR on Snippet Deletion
Overview
- Platform: BugForge
- Vulnerability: Insecure Direct Object Reference (IDOR) — missing authorization check on snippet deletion
- Key Technique: Exploiting inconsistent authorization between PUT and DELETE endpoints on the same resource
- Result: Deleted another user’s snippet using own JWT, flag returned in response
Objective
Find and exploit a vulnerability in the BugForge “Copypasta” snippet-sharing application.
Initial Access
# Target Application
URL: https://lab-1773864769112-56dr05.labs-app.bugforge.io
# Auth details
POST /api/register with {username, password}
Returns JWT Bearer token (HS256, no expiry)
Registered as: haxor (id:5, role: user)
Key Findings
- IDOR on DELETE /api/snippets/:id (CWE-639: Authorization Bypass Through User-Controlled Key) — The DELETE endpoint authenticates the user (requires valid JWT) but does not verify that the authenticated user owns the snippet being deleted. Any authenticated user can delete any snippet by ID. The PUT endpoint on the same resource correctly checks ownership and returns 403 — the inconsistency indicates the authorization check was simply missed on DELETE.
Attack Chain Visualization
┌──────────────┐ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Register │────▶│ Enumerate App │────▶│ Test IDOR on │────▶│ Test IDOR on │
│ (haxor) │ │ API surface │ │ PUT /snippets/1 │ │ DELETE /snip/1 │
│ Get JWT │ │ via Caido │ │ │ │ │
└──────────────┘ └──────────────────┘ │ 403 Forbidden │ │ 200 OK + FLAG │
│ "Not authorized │ │ Ownership NOT │
│ to edit" │ │ checked │
└──────────────────┘ └──────────────────┘
Application Architecture
| Component | Detail |
|---|---|
| Backend | Express (Node.js) |
| Frontend | React SPA |
| Auth | JWT HS256 — payload: {id, username, iat}, no expiry |
| Database | SQLite (inferred) |
| CORS | Access-Control-Allow-Origin: * |
API Surface
| Endpoint | Method | Auth | Authorization |
|---|---|---|---|
| /api/register | POST | No | — |
| /api/verify-token | GET | Yes | — |
| /api/snippets | GET | Yes | Returns own snippets |
| /api/snippets | POST | Yes | Creates under own user |
| /api/snippets/public | GET | No | — |
| /api/snippets/:id | PUT | Yes | Ownership check (403) |
| /api/snippets/:id | DELETE | Yes | NO ownership check (VULN) |
| /api/snippets/share/:uuid | GET | No | — |
| /api/snippets/:id/like | POST | Yes | — |
| /api/snippets/:id/comments | GET/POST | Yes | — |
| /api/profile | PUT | Yes | Own profile only |
| /api/profile/password | PUT | Yes | No old password required |
| /api/profile/:username | GET | No | — |
Known Users
| Username | ID | Notable |
|---|---|---|
| admin | 1 | Snippet 7 (SQL query) |
| coder123 | 2 | Snippets 1, 2 |
| pythonista | 3 | Snippet 3 |
| webdev | 4 | Snippets 5, 6 |
| haxor | 5 | Our user |
Exploitation Path
Step 1: Register and Authenticate
POST /api/register HTTP/1.1
Content-Type: application/json
{
"username": "haxor",
"password": "password123"
}
Response returns JWT with payload {"id":5,"username":"haxor","iat":...}. Role is stored server-side as “user” — not included in the token.
Step 2: Enumerate API Surface
Walked the application in browser and reviewed Caido proxy history to map all API endpoints. Key observation: snippets support full CRUD operations (GET, POST, PUT, DELETE) with JWT authentication required for all mutations.
Step 3: Test IDOR on PUT (Blocked)
PUT /api/snippets/1 HTTP/1.1
Authorization: Bearer <haxor_jwt>
Content-Type: application/json
{
"title": "pwned",
"content": "owned by haxor"
}
Response: 403 Forbidden — “Not authorized to edit this snippet”
The PUT endpoint correctly verifies that the authenticated user (id:5) owns snippet 1 (owned by coder123, id:2).
Step 4: Test IDOR on DELETE (Vulnerable)
DELETE /api/snippets/1 HTTP/1.1
Authorization: Bearer <haxor_jwt>
Response: 200 OK — snippet deleted, flag returned in response body.
The DELETE endpoint authenticates the request (valid JWT required) but does not check whether the requesting user owns the target snippet. The authorization check present on PUT was not implemented on DELETE.
Flag / Objective Achieved
| Vector | Flag |
|---|---|
| IDOR on DELETE /api/snippets/1 | bug{sRIsMVR8YIzpa1juLkeIXizU9LpQcQgW} |
Key Learnings
- Authorization must be consistent across all HTTP methods on a resource. The PUT endpoint had a proper ownership check, but DELETE did not. When different methods on the same resource enforce different authorization rules, the weakest one becomes the attack vector.
- Authentication is not authorization. The DELETE endpoint required a valid JWT (authentication) but didn’t verify the user had permission to delete the specific resource (authorization). These are separate concerns that must both be addressed.
- Test all CRUD operations independently. A 403 on PUT doesn’t mean DELETE is also protected. Each HTTP method may have its own handler with its own (or missing) authorization logic.
- Systematic API enumeration pays off. Mapping the full API surface via proxy history revealed the inconsistency. Testing only the “obvious” endpoints would have missed the vulnerability.
Failed Approaches
| Approach | Result | Why It Failed |
|---|---|---|
| IDOR on PUT /api/snippets/:id | 403 Forbidden | Ownership check correctly implemented on PUT |
| Mass assignment on register (role: admin) | Role ignored | Server filters to whitelisted fields only |
| Mass assignment on profile update (role: admin) | Request accepted, role unchanged | PUT /api/profile uses field whitelist |
Tools Used
| Tool | Purpose |
|---|---|
| Caido | HTTP proxy — API enumeration and request replay |
| curl / HTTP client | Direct API endpoint testing |
| Browser DevTools | React SPA analysis, frontend review |
Remediation
1. Missing Authorization on DELETE Endpoint (CVSS: 7.1 - High)
Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:L
Issue: The DELETE /api/snippets/:id endpoint authenticates the user but does not verify ownership of the target snippet. Any authenticated user can delete any snippet in the system.
CWE Reference: CWE-639 — Authorization Bypass Through User-Controlled Key
Fix:
// BEFORE (Vulnerable)
router.delete('/snippets/:id', authMiddleware, async (req, res) => {
const snippet = await Snippet.findByPk(req.params.id);
if (!snippet) return res.status(404).json({ error: 'Not found' });
await snippet.destroy();
res.json({ message: 'Deleted' });
});
// AFTER (Secure)
router.delete('/snippets/:id', authMiddleware, async (req, res) => {
const snippet = await Snippet.findByPk(req.params.id);
if (!snippet) return res.status(404).json({ error: 'Not found' });
if (snippet.user_id !== req.user.id) {
return res.status(403).json({ error: 'Not authorized to delete this snippet' });
}
await snippet.destroy();
res.json({ message: 'Deleted' });
});
Additional recommendations:
- Extract the ownership check into shared middleware used by both PUT and DELETE handlers. This prevents future inconsistencies when adding new methods.
- Consider implementing role-based overrides (admin can delete any snippet) separately from the ownership check.
2. Password Change Without Old Password Verification (CVSS: 5.4 - Medium)
Vector: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:N
Issue: PUT /api/profile/password changes the password without requiring the current password. If an attacker gains a valid session (e.g., through XSS or session fixation), they can lock out the legitimate user.
CWE Reference: CWE-620 — Unverified Password Change
Fix:
// BEFORE (Vulnerable)
router.put('/profile/password', authMiddleware, async (req, res) => {
const { new_password } = req.body;
await user.update({ password: hash(new_password) });
});
// AFTER (Secure)
router.put('/profile/password', authMiddleware, async (req, res) => {
const { old_password, new_password } = req.body;
if (!await verify(old_password, user.password)) {
return res.status(403).json({ error: 'Current password incorrect' });
}
await user.update({ password: hash(new_password) });
});
OWASP Top 10 Coverage
- A01:2021 — Broken Access Control: The core vulnerability. The DELETE endpoint enforces authentication but not authorization, allowing horizontal privilege abuse (one user acting on another user’s resources).
- A04:2021 — Insecure Design: Inconsistent authorization across HTTP methods on the same resource indicates a design-level gap — authorization was not applied systematically.
References
- CWE-639: Authorization Bypass Through User-Controlled Key
- CWE-620: Unverified Password Change
- OWASP Testing Guide: IDOR Testing
- OWASP Top 10:2021 — Broken Access Control
Tags: #idor #broken-access-control #authorization-bypass #bugforge #webapp
Document Version: 1.0
Last Updated: 2026-03-18