Stored XSS via product descriptions rendered with bypassSecurityTrustHtml
Product descriptions fetched from the backend are wrapped with `bypassSecurityTrustHtml` and rendered with `[innerHTML]`; because PUT `/api/Products/:id` is left unauthenticated server-side, any user can persist an XSS payload in a product description.
trustProductDescription marks every description field as Safe HTML, defeating Angular's built-in escaping for that value:
trustProductDescription (tableData: any[]) {
for (let i = 0; i < tableData.length; i++) {
tableData[i].description = this.sanitizer.bypassSecurityTrustHtml(tableData[i].description)
}
}
The trusted descriptions are then rendered as raw HTML in frontend/src/app/product-details/product-details.component.html:16:
<div [innerHTML]="data.productData.description"></div>
Product descriptions are an attacker-controlled stored field — in server.ts:364 the auth middleware for product mutation is commented out:
app.post('/api/Products', security.isAuthorized())
// app.put('/api/Products/:id', security.isAuthorized())
leaving the underlying Sequelize-backed PUT route open to anyone. There is no DOMPurify / sanitize-html step anywhere on the path from request body → DB → render.
- As any unauthenticated user, persist an XSS payload into a product description:
PUT /api/Products/1 HTTP/1.1
Host: <host>
Content-Type: application/json
{"description":"<iframe src=javascript:alert(document.cookie)>"}
- Any user who later opens the product details dialog (clicking the product on
/#/search) triggers script execution becausedata.productData.descriptionis rendered via[innerHTML]afterbypassSecurityTrustHtml.
Persistent cross-site scripting affecting every visitor of the product. Attackers can run arbitrary JavaScript in victims' origins, exfiltrate the JWT held in localStorage, hijack admin sessions, alter basket / order data, or pivot to other authenticated APIs. The stored payload survives across page loads.
The trustProductDescription method explicitly calls this.sanitizer.bypassSecurityTrustHtml(tableData[i].description) on every product description, which defeats Angular's built-in sanitization when rendered via [innerHTML] in product-details. Combined with the unauthenticated PUT /api/Products/:id route cited by the detector, an attacker can persist arbitrary HTML/JS into descriptions that fires for every viewer. The scope explicitly states Juice Shop should be evaluated as a real production app, so the deliberate-vulnerability nature does not change the verdict. The sink, source, and exploit chain are all directly evidenced in the supplied code.
The payload is planted via an unauthenticated network request — server.ts:364 shows app.put('/api/Products/:id', security.isAuthorized()) is commented out, so PR:N and AV:N. Execution happens when a victim opens the product details dialog rendered with [innerHTML] after trustProductDescription calls bypassSecurityTrustHtml, so UI:R. Stored XSS crosses from the server component into the victim's browser origin (S:C); within that origin the attacker can read the JWT from localStorage and forge requests, but XSS classically gives partial, not total, control of data in the browser, so C:L/I:L/A:N.
- CWE-79
- CWE-79: Persistent XSS
- OWASP A03:2021 Injection