agentggagentgg
Back to all findings
MEDIUMconfirmedxssxss9e89e2bdea96

Stored XSS via [innerHTML] of bypassSecurityTrustHtml-wrapped feedback comment in About page

The customer-feedback gallery renders `item?.args` directly via Angular `[innerHTML]`, and the component populates `args` from raw user-submitted feedback comments wrapped in `DomSanitizer.bypassSecurityTrustHtml`, allowing stored XSS in any visitor to the About page.

Filefrontend/src/app/about/about.component.html
Lines5053
Confidence
90%
File statusvalidated
Details

about.component.html L50-53:

<ng-container *galleryImageDef="let item; let active = active">
  @if (active) {
    <figure class="feedback" [innerHTML]="item?.args"></figure>
  }
</ng-container>

item.args is populated in about.component.ts L116-126:

feedbacks[i].comment = `<figcaption><p class="feedback-comment">${feedbacks[i].comment}</p>...`
feedbacks[i].comment = this.sanitizer.bypassSecurityTrustHtml(feedbacks[i].comment)
this.galleryRef.addImage({ src: ..., args: feedbacks[i].comment })

The feedback comment is interpolated into an HTML string and then explicitly trust-marked — bypassSecurityTrustHtml disables Angular's default escaping. The upstream sanitization in models/feedback.ts only runs security.sanitizeHtml / security.sanitizeSecure (a recursive wrapper around sanitize-html), and when the persistedXssFeedbackChallenge flag is active it uses the deliberately-bypassable legacy sanitizer; payloads such as <<script>script>alert(1)</script> survive. Even with the recursive variant, sanitize-html configurations are routinely bypassable and the trust marker means there is no second-chance encoding on the client.

Proof of concept
  1. Submit a review with payload comment, e.g., POST /api/Feedbacks { rating: 5, comment: "<iframe src=javascript:alert(document.cookie)>" }.
  2. Wait for the comment to surface in the feedback gallery on /#/about. The iframe is injected into the DOM via [innerHTML] of the bypass-trusted value and executes in the visitor's origin.
Impact

Any visitor to the About page (unauthenticated) executes attacker script in the Juice Shop origin, enabling session-cookie theft, action-on-behalf-of-victim, and credential phishing.

Validation
confirmed

The about.component.html line 52 binds [innerHTML]="item?.args" and the finding accurately describes that args is populated from user-submitted feedback comments wrapped with DomSanitizer.bypassSecurityTrustHtml, which disables Angular's built-in escaping. This is the classic Juice Shop persistedXssFeedbackChallenge sink, and the scope explicitly directs reviewers to treat intentionally-vulnerable Juice Shop code as a real production bug. The exploit path (POST /api/Feedbacks → comment rendered into innerHTML of every About-page visitor) is reachable from an unauthenticated attacker and runs in the app origin.

CVSS 3.1
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
Base score: 6.1 · MEDIUM

The attacker reaches the sink by POSTing to /api/Feedbacks over the network with no auth check shown in the finding (PR:N worst-case), and the payload is rendered via [innerHTML]="item?.args" where args was wrapped in bypassSecurityTrustHtml, so Angular performs no client-side escaping — making exploitation straightforward once upstream sanitize-html (or the deliberately-bypassable legacy sanitizer under persistedXssFeedbackChallenge) is bypassed (AC:L). A victim must browse to /#/about for the gallery to render the stored payload (UI:R), and the injected script then executes in the Juice Shop origin under the victim's authority, which CVSS treats as Scope Changed for stored XSS. Impact is L/L/N: attacker JS can read non-HttpOnly session/auth data and perform limited actions as the victim, but does not yield full account/system takeover or affect availability.

References