agentggagentgg
Back to all findings
MEDIUMconfirmedxssxss9bf2ef893cf2

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.

Filefrontend/src/app/search-result/search-result.component.ts
Lines130142
Confidence
95%
File statusvalidated
Details

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.

Proof of concept

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.

Impact

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.

Validation
confirmed

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.

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

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).

References