Reflected DOM XSS via `q` query parameter in search results
The `q` query parameter is passed through `DomSanitizer.bypassSecurityTrustHtml` and then rendered via `[innerHTML]` in the template, producing a reflected DOM-based XSS.
In filterTable(), the raw query string is read straight from the URL and wrapped with Angular's HTML-trust escape hatch:
let queryParam: string = this.route.snapshot.queryParams.q
if (queryParam) {
queryParam = queryParam.trim()
...
this.dataSource.filter = queryParam.toLowerCase()
this.searchValue = this.sanitizer.bypassSecurityTrustHtml(queryParam)
...
}
The resulting SafeHtml is then rendered as raw HTML in search-result.component.html:
<span id="searchValue" [innerHTML]="searchValue"></span>
Because bypassSecurityTrustHtml explicitly disables Angular's contextual escaping and no sanitizer (DOMPurify, sanitize-html, etc.) is applied, any attacker-controlled URL query string is interpreted as HTML. Even though <script> tags are stripped by browsers when inserted via innerHTML, vectors like <iframe src="javascript:alert(1)">, <img src=x onerror=alert(1)>, or <svg/onload=...> execute. This is the classic OWASP Juice Shop "DOM XSS" challenge.
Browse to:
http://<host>/#/search?q=<iframe src="javascript:alert(xss)">
The iframe is injected into <span id="searchValue"> and JavaScript executes in the victim's browser session.
Any attacker who can convince a victim to follow a crafted search URL can execute arbitrary JavaScript in the victim's authenticated origin: session-token theft (the app stores its JWT in localStorage and reads it as localStorage.getItem('token')), basket manipulation, and pivoting to admin functions if the victim is an admin. No authentication is required to deliver the payload.
The filterTable() method reads this.route.snapshot.queryParams.q (attacker-controlled URL input) and passes it directly to this.sanitizer.bypassSecurityTrustHtml(queryParam), storing the result in searchValue which is rendered via [innerHTML] in the template. bypassSecurityTrustHtml explicitly disables Angular's contextual escaping, so payloads like <iframe src="javascript:..."> or <img src=x onerror=...> execute. The scope rule says to treat Juice Shop as a real production app, so this is a genuine reflected DOM XSS.
In filterTable(), the unvalidated q query parameter is passed to this.sanitizer.bypassSecurityTrustHtml(queryParam) and rendered through [innerHTML]="searchValue", so any attacker-crafted URL (e.g. <iframe src="javascript:alert(1)">) executes JS in the victim's authenticated origin. The search route is publicly reachable (no guard in the snippet) so PR=N, but a victim must click the crafted link (UI=R). Per CVSS 3.1 guidance for reflected XSS, Scope is Changed because script execution crosses from the vulnerable Angular component into the victim's browser/session authority, where it can read localStorage.getItem('token') (C:L) and perform authenticated actions like basket manipulation (I:L); no DoS primitive is evident (A:N).
- CWE-79
- OWASP A03:2021 Injection