August 8, 2026
Search Has Escaped
Search is supposed to live in the address bar.

By Leonardo R Cavalcante
2 min read
It is long and ugly there, but it is contained. People may type into it and Search may return a document, or perhaps several million documents that do not contain the thing you asked for. This is the old arrangement.
On Tuesday, Search got into the page.
It happened on the results screen. When nothing matched, the application repeated the query back to the user so they would know exactly what had failed them.
const q = new URLSearchParams(location.search).get('q') ?? '';
message.innerHTML = `No results for <strong>${q}</strong>`;const q = new URLSearchParams(location.search).get('q') ?? '';
message.innerHTML = `No results for <strong>${q}</strong>`;The query was obtained from location.search. This is the place in JavaScript where Search can be observed in captivity. The application then placed it into innerHTML.
I do not know why.
Some people believe the <strong> element makes this safe. It does not. <strong> is a means of making letters thick. It has never stopped a crime and has no legal powers.
I sent the following URL:
/search?q=%3Cimg%20src%3Dx%20onerror%3Dalert(document.domain)%3E/search?q=%3Cimg%20src%3Dx%20onerror%3Dalert(document.domain)%3EThe strange symbols are percent-encoding. They allow unpleasant characters to travel inside a URL wearing little cardboard boxes. URLSearchParams opens the boxes. What comes out is this:
<img src=x onerror=alert(document.domain)><img src=x onerror=alert(document.domain)>There is no image at x, which is normal because x is not a place. The image fails to load. Its onerror handler runs. Search now causes JavaScript to execute.
This is DOM XSS. The attacker-controlled value is exposed through a browser source, crosses the page's JavaScript and reaches an HTML-parsing sink. The backend does not need to store it or render it. It may spend the entire incident doing nothing and will later receive several emails.
The alert is only a noise made by researchers so that nearby people will know a finding has occurred. If I send the crafted link to another user and they open it, my code runs in the application's origin and their session. I can make authenticated requests, read responses exposed to the frontend, alter the page and capture information entered after the payload runs.
Search has become venomous.
There is no <script> tag in the payload. This troubles committees that have spent years preparing specifically for the arrival of a <script> tag. Event handlers execute JavaScript too. The browser has known this for a long time and did not think to mention it at the meeting.
The defect is not that the query contains an image, an event handler or the word alert. The defect is that a string controlled by the URL is handed to an HTML parser when the page only wanted to display text.
Search should not be parsed. Search has no markup to contribute.
const q = new URLSearchParams(location.search).get('q') ?? '';
const strong = document.createElement('strong');
strong.textContent = q;
message.replaceChildren('No results for ', strong);const q = new URLSearchParams(location.search).get('q') ?? '';
const strong = document.createElement('strong');
strong.textContent = q;
message.replaceChildren('No results for ', strong);textContent places the query in a text node. The angle brackets remain angle brackets. The image is not born. Its event handler receives no body and no opportunities.
The page still says there are no results.
— — — —
This article was originally published at malloc.com.br