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.
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.
- Submit a review with payload comment, e.g.,
POST /api/Feedbacks { rating: 5, comment: "<iframe src=javascript:alert(document.cookie)>" }. - 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.
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.
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.
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.
- CWE-79
- OWASP Top 10 A03:2021