XSS via [innerHTML] binding of product description in product-details dialog
Product description is rendered through `[innerHTML]` and the caller (`search-result.component.ts`) supplies a `SafeHtml` value produced by `bypassSecurityTrustHtml`, disabling Angular's default sanitization and enabling stored XSS through product description content.
In product-details.component.html line 16:
<div [innerHTML]="data.productData.description"></div>
The data.productData object is the dialog's MAT_DIALOG_DATA (lines 38–40 of product-details.component.ts). When the dialog is opened from search-result (the most common path), the description field has already been marked safe by search-result.component.ts lines 107–111:
trustProductDescription (tableData: any[]) {
for (let i = 0; i < tableData.length; i++) {
tableData[i].description = this.sanitizer.bypassSecurityTrustHtml(tableData[i].description)
}
}
The description ultimately comes from the products API, which can be modified by an attacker via the documented administrative routes / SQL injection / direct PUT /api/Products/:id endpoint in Juice Shop. Because the value is wrapped as SafeHtml, the [innerHTML] binding renders any HTML/JS the attacker stored, producing stored XSS in every user who opens the affected product details dialog.
- As an attacker with the ability to modify a product (e.g., exploit the
PUT /api/Products/:idauthorization bypass in Juice Shop), setdescriptionto<iframe src="javascript:alert(xss)">. - Any user who searches/browses to the product and opens the details dialog from search results will have the iframe rendered through
[innerHTML]and the JS will execute.
Stored XSS visible to every user viewing the affected product details from search results. Executes arbitrary JavaScript in victim browsers, enabling account takeover via token exfiltration from localStorage, action-on-behalf, and propagation.
The template at line 16 binds [innerHTML]="data.productData.description", and the caller search-result.component.ts pre-wraps every description via this.sanitizer.bypassSecurityTrustHtml(...), which explicitly disables Angular's built-in HTML sanitizer for this binding. Because the description originates from the products API and is mutable through the known Juice Shop PUT /api/Products/:id authorization-bypass route (and SQLi), an attacker-controlled payload (e.g., <iframe src="javascript:...">) will render verbatim in every user's dialog, yielding stored XSS. The scope explicitly instructs treating Juice Shop as a real production app, so this is in-scope and the exploit chain is plausible.
The [innerHTML]="data.productData.description" binding on line 16 renders attacker-controlled HTML because search-result.component.ts wraps the description with bypassSecurityTrustHtml, defeating Angular's sanitizer. Reaching the sink requires the attacker to first persist a malicious description through the Products API (the finding cites the PUT /api/Products/:id authorization-bypass path in Juice Shop), which in practice needs a low-privileged authenticated user (PR:L), and then a victim must open the product-details dialog from search results (UI:R). Scope is Changed because the server-side stored payload executes in the victim's browser security context, enabling token theft from localStorage (Juice Shop's JWT) and actions on behalf of the victim — yielding high C/I and some availability impact (UI freezes, forced redirects). If PUT /api/Products/:id is reachable fully unauthenticated in the deployed configuration, PR could drop to N.
- CWE-79
- OWASP A03:2021 Injection
- https://angular.io/api/platform-browser/DomSanitizer#bypassSecurityTrustHtml