agentggagentgg
Back to all findings
CRITICALconfirmedxssxssac5cf9bf4347

Stored XSS via [innerHTML] of user.email in administration user table

The admin user table renders `user.email` through Angular `[innerHTML]`, and the component pre-wraps every email in `DomSanitizer.bypassSecurityTrustHtml` after string-interpolating it into an HTML span — emails containing HTML are executed in the admin's browser.

Filefrontend/src/app/administration/administration.component.html
Lines2527
Confidence
90%
File statusvalidated
Details

administration.component.html L25-27 and L112-114:

<mat-cell *matCellDef="let user" [innerHTML]="user.email" class="cell-middle"></mat-cell>

administration.component.ts L73:

user.email = this.sanitizer.bypassSecurityTrustHtml(`<span class="${this.doesUserHaveAnActiveSession(user) ? 'confirmation' : 'error'}">${user.email}</span>`)

The email is concatenated into raw HTML and trust-marked. Upstream, models/user.ts only applies security.sanitizeSecure when persistedXssUserChallenge is disabled; when the challenge flag is enabled the email setter skips sanitization entirely (L60-72). The Juice Shop ships with this challenge enabled, so registration with email: "<iframe src=\"javascript:alert('xss')\">" is persisted verbatim. Even when sanitized, the trust marker plus interpolation into <span class="…">${email}</span> permits class-attribute breakouts.

The same sink is duplicated in the hidden table at L112-114, doubling the rendering.

Proof of concept
  1. Register a user (POST /api/Users) with email: "<iframe src=\"javascript:alert(xss)\">".
  2. Log in as admin and visit /#/administration. The iframe payload executes when the admin user table renders the malicious email through [innerHTML].
Impact

Stored XSS that executes in the administrator's browser, leading to admin-session compromise. Any registrant can attack the admin without further interaction.

Validation
confirmed

The template uses [innerHTML]="user.email" at lines 25-27 (and duplicated 112-114), and the component wraps the email with DomSanitizer.bypassSecurityTrustHtml after string-interpolating it into a <span class="…">${user.email}</span> template. The bypass marker tells Angular to skip sanitization, so any HTML/script-equivalent payload (e.g., <iframe> with a JS sink, <img onerror>) in the email field executes when the admin renders the table. The User model's setter skips sanitizeSecure when the persistedXssUserChallenge flag is enabled, allowing an attacker to register an email containing the payload via POST /api/Users and have it stored verbatim. Scope explicitly directs us to treat Juice Shop findings as real production bugs, so the exploit chain (untrusted registration input → admin DOM execution) is confirmed.

CVSS 3.1
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:L
Base score: 9.6 · CRITICAL

The payload is delivered via the unauthenticated POST /api/Users registration endpoint (PR:N, AV:N), and the persistedXssUserChallenge-enabled code path in models/user.ts skips sanitization while administration.component.ts L73 explicitly wraps the email in bypassSecurityTrustHtml, so no special conditions beyond registering are needed (AC:L). The sink only fires when an administrator navigates to /#/administration and the [innerHTML]="user.email" cell renders (UI:R). Execution occurs in the admin's authenticated origin — a different security authority than the anonymous attacker — making this Scope:Changed; full admin-session hijack enables reading and modifying any admin-controlled resource (C:H/I:H), with secondary availability impact (e.g., destructive admin actions or page disruption) rated L since DoS is not the primary outcome.

References