From 0e97b155ff1b15bc3173118561316d8ea28ec9b7 Mon Sep 17 00:00:00 2001
From: Emmanuele Bassi <ebassi@gnome.org>
Date: Fri, 10 Oct 2025 17:06:22 +0100
Subject: [PATCH] Make sure to escape query strings

Unescaped query strings should not be passed to the HTML parser, to
avoid unwanted execution of JavaScript.

The query is shown in the header of the search results, so we can easily
split the header from the results; then we use a plain text node to
represent the query, and let the browser escape it.

See: https://cheatsheetseries.owasp.org/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.html

Fixes: #228

CVE: CVE-2025-11687
Upstream-Status: Backport [https://gitlab.gnome.org/GNOME/gi-docgen/-/commit/c53d2640bfa5823bbdf33683d95c160267c0ec68]

Signed-off-by: Zhang Peng <peng.zhang1.cn@windriver.com>
---
 gidocgen/templates/basic/search.js | 30 +++++++++++++++++++-----------
 1 file changed, 19 insertions(+), 11 deletions(-)

diff --git a/gidocgen/templates/basic/search.js b/gidocgen/templates/basic/search.js
index 29c204f..628f0a6 100644
--- a/gidocgen/templates/basic/search.js
+++ b/gidocgen/templates/basic/search.js
@@ -182,17 +182,24 @@ function hideSearchResults() {
     }
 }
 
-function renderResults(query, results) {
-    let html = "";
+function createResultsTitle(query, n_results) {
+    // Ensure we're returning an escaped query string, to ensure we
+    // prevent XSS vulnerabilities
+    let h1 = document.createElement("h1");
+    let text = document.createTextNode("Results for “" + query + "” (" + n_results + ")");
+    h1.appendChild(text)
+    return h1;
+}
 
-    html += "<h1>Results for &quot;" + query + "&quot; (" + results.length + ")</h1>" +
-                "<div id=\"search-results\">"
+function createResultsContent(results) {
+    let search_results = document.createElement("div");
+    search_results.setAttribute("id", "search-results");
 
     if (results.length === 0) {
-        html += "No results found.";
+        search_results.textContent = "No results found.";
     }
     else {
-        html += "<div class=\"results\"><dl>";
+        let html = "<div class=\"results\"><dl>";
         results.forEach(function(item) {
             html += "<dt class=\"result " + TYPE_CLASSES[item.type] + "\">" +
                       "<a href=\"" + item.href + "\">" + item.text + "</a>" +
@@ -204,11 +211,11 @@ function renderResults(query, results) {
                     "<dd>" + item.summary + "</dd>";
         });
         html += "</dl></div>";
-    }
 
-    html += "</div>";
+        search_results.innerHTML = html;
+    }
 
-    return html;
+    return search_results;
 }
 
 function showResults(query, results) {
@@ -218,9 +225,10 @@ function showResults(query, results) {
         window.history.replaceState(refs.input.value, "", baseUrl + extra + window.location.hash);
     }
 
-    window.title = "Results for: " + query;
+    window.title = "Results for “" + query + "” (" + results.length + ")";
     window.scroll({ top: 0 })
-    refs.search.innerHTML = renderResults(query, results);
+    refs.search.appendChild(createResultsTitle(query, results.length));
+    refs.search.appendChild(createResultsContent(results));
     showSearchResults(search);
 }
 
-- 
2.50.0

