agentggagentgg
Back to all findings
MEDIUMconfirmedxssxss654e2844fc4c

XSS via bypassSecurityTrustHtml on user-controlled feedback comments in AboutComponent

Feedback comments fetched from the backend are interpolated into an HTML string and then marked trusted via DomSanitizer.bypassSecurityTrustHtml, allowing stored XSS in the About page gallery.

Filefrontend/src/app/about/about.component.ts
Lines111124
Confidence
90%
File statusvalidated
Details

In populateSlideshowFromFeedbacks() the component subscribes to feedbackService.find() and for each feedback record constructs a raw HTML string by interpolating the untrusted feedbacks[i].comment field, then bypasses Angular's built-in sanitizer:

feedbacks[i].comment = `<figcaption><p class="feedback-comment">${
  feedbacks[i].comment
}</p><div class="feedback-stars">(${this.stars[feedbacks[i].rating]})</div></figcaption>`
feedbacks[i].comment = this.sanitizer.bypassSecurityTrustHtml(
  feedbacks[i].comment
)

this.galleryRef.addImage({
  src: this.images[i % this.images.length],
  args: feedbacks[i].comment
})

Feedback rows are user-submitted content (anyone may POST a feedback comment via the public /api/Feedbacks endpoint), so the comment field is fully attacker-controlled. The string is wrapped in raw HTML, no sanitizer (DOMPurify/sanitize-html) is invoked on the same path, and bypassSecurityTrustHtml defeats Angular's default escaping. The resulting SafeHtml is then rendered by ng-gallery as the image caption.

This is the classic Angular XSS pattern: untrusted input + bypassSecurityTrustHtml.

Proof of concept
  1. As any visitor (no auth needed) submit a feedback containing <img src=x onerror=alert(document.domain)> via POST /api/Feedbacks { comment, rating, captchaId, captcha }.
  2. Navigate to /about in the application.
  3. populateSlideshowFromFeedbacks fetches the new feedback, interpolates the comment into the figcaption HTML, calls bypassSecurityTrustHtml, and hands the SafeHtml to ng-gallery, which renders it — executing the script in every visitor's browser.
Impact

Stored XSS on a public, unauthenticated page (/about). An attacker can submit a feedback payload once and have arbitrary JavaScript execute in the browser of every visitor — including administrators who later browse the page — enabling session hijacking via cookie or localStorage token theft, CSRF on behalf of the victim, and account takeover.

Validation
confirmed

The component takes feedbacks[i].comment (attacker-controlled via the public Feedbacks API), template-literal-interpolates it into raw HTML, and explicitly wraps it with this.sanitizer.bypassSecurityTrustHtml(...) before passing it to galleryRef.addImage({args: ...}), which ng-gallery renders via the imported GalleryImageDef template. This is the textbook Angular stored-XSS pattern: untrusted input concatenated into HTML plus an explicit sanitizer bypass, with no DOMPurify/sanitize-html on this path. Per scope, the training-app nature does not downgrade the finding. Exploitation requires only an unauthenticated feedback POST and a visit to /about.

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 payload is delivered over HTTP to the public /api/Feedbacks endpoint (AV:N) and the PoC explicitly notes "no auth needed" to seed the stored comment (PR:N); although a captchaId/captcha is referenced in the POST body, no captcha verification is visible in this file and Juice Shop's captcha is a trivial arithmetic challenge, so AC:L. Execution still requires a victim to navigate to /about where populateSlideshowFromFeedbacks runs bypassSecurityTrustHtml on the attacker-controlled feedbacks[i].comment and hands it to ng-gallery (UI:R). Stored XSS executing in the victim's browser crosses from the server component into the user-agent's security authority (S:C); the injected script can read cookies/localStorage tokens and perform authenticated actions on behalf of the visitor, giving partial confidentiality and integrity impact (C:L/I:L), with no direct availability impact (A:N).

References