Skip to main content
Vulnerability Database/CVE-2026-90583

CVE-2026-90583: Kagisearch Smallweb XSS Vulnerability

CVE-2026-90583 is a cross-site scripting vulnerability in Kagisearch Smallweb affecting query string rendering. Exploitation requires raw HTTP requests with unencoded characters. This article covers technical details, impact, and mitigation.

Published:

CVE-2026-90583 Overview

CVE-2026-90583 is a reflected cross-site scripting (XSS) vulnerability [CWE-79] in kagisearch/smallweb, the open-source frontend used by the Kagi Small Web project. The flaw resides in the index function of app/sw.py within the Query String Rendering component. An attacker who supplies unencoded double-quote characters in the qs query parameter can inject arbitrary HTML or JavaScript into the rendered page. Because the project follows a rolling release model, no fixed version tag exists; all commits up to 0ecb9c48edbf98dc7e934b54fbac43869e64b4cf are affected. The upstream fix is committed as 00b68144e583f20a6b67e29cf01bc07f57979ffb.

Critical Impact

Successful exploitation allows script injection in a victim's browser session, enabling session hijacking, credential theft, or client-side action forgery against Small Web instances.

Affected Products

  • kagisearch/smallweb commits up to 0ecb9c48edbf98dc7e934b54fbac43869e64b4cf
  • The app/sw.pyindex function (Query String Rendering component)
  • Jinja templates app/templates/partials/flag_panel.html and app/templates/partials/mobile_more.html

Discovery Timeline

  • 2026-09-13 - CVE-2026-90583 published to NVD
  • 2026-09-15 - Last updated in NVD database

Technical Details for CVE-2026-90583

Vulnerability Analysis

The vulnerability is a reflected XSS issue in the query-string handling path of kagisearch/smallweb. The application reads the raw HTTP request query string via Werkzeug's request.query_string and passes the value into a Jinja2 template as the qs variable. Two templates rendered that variable with the |safe filter, which disables HTML auto-escaping. As a result, any HTML metacharacters present in the raw query string are written directly into the page.

Exploitation requires a raw HTTP request that includes an unencoded double-quote (") character in the query string. Standard browsers percent-encode " as %22, so the payload must be delivered through tools such as curl, netcat, or a scripted client rather than a normal browser link. Once rendered, the injected content escapes the surrounding action attribute of the <form> tag and executes in the victim's session context.

Root Cause

The root cause is the use of the Jinja2 |safe filter on attacker-controlled input. The templates rendered {{ qs|safe }} inside a form action attribute, marking the value as trusted HTML and bypassing Jinja2's default auto-escaping. Combined with Werkzeug returning the raw request-target from request.query_string, unsanitized bytes flowed from the HTTP request into the DOM.

Attack Vector

The attack is network-reachable and unauthenticated, but requires user interaction to load a crafted request. An attacker sends a raw HTTP request whose query string contains double-quote characters and an injected HTML payload, then lures a victim into visiting the resulting URL through non-browser tooling or a chained redirect that preserves the raw bytes.

text
// Patch: app/templates/partials/flag_panel.html
 <p class="flag-dropdown-text" id="flag-dropdown-text">Flag this post as inappropriate?</p>
-<form action="{{ prefix }}flag_content?{{ qs|safe }}" method="post" class="flag-dropdown-form">
+<form action="{{ prefix }}flag_content?{{ qs }}" method="post" class="flag-dropdown-form">
   <input type="hidden" name="url" value="{{ source_url|default(url, true) }}">
   <button type="submit" class="dropdown-action" data-dropdown-autofocus>Confirm</button>
 </form>

// Patch: app/templates/partials/mobile_more.html
-<form action="{{ prefix }}flag_content?{{ qs|safe }}" method="post" class="mobile-flag-form">
+<form action="{{ prefix }}flag_content?{{ qs }}" method="post" class="mobile-flag-form">

Source: GitHub Commit 00b68144. The fix removes the |safe filter so Jinja2's default HTML auto-escaping sanitizes the qs value before rendering.

Detection Methods for CVE-2026-90583

Indicators of Compromise

  • HTTP requests to smallweb endpoints containing raw " (0x22) characters in the query string rather than %22.
  • Query strings containing <script>, onerror=, onload=, javascript:, or attribute-breakout patterns targeting the flag_content route.
  • Access log entries where User-Agent values suggest scripted clients (curl, python-requests, custom tooling) paired with malformed query strings.

Detection Strategies

  • Inspect web server or reverse-proxy logs for request-target bytes outside the RFC 3986 unreserved set, particularly unencoded quotes and angle brackets.
  • Deploy a Web Application Firewall (WAF) rule that blocks or normalizes raw quote characters in query strings destined for sw.py handlers.
  • Compare rendered HTML responses against a known-good baseline to identify unexpected attributes or injected <script> nodes in flag_content forms.

Monitoring Recommendations

  • Alert on outbound requests from user browsers to unknown domains immediately after loading a smallweb URL, which may indicate exfiltration by injected script.
  • Track error rates and template rendering anomalies in application logs following requests with unusual query-string content.
  • Monitor the upstream repository at kagisearch/smallweb for additional advisories tied to the rolling release stream.

How to Mitigate CVE-2026-90583

Immediate Actions Required

  • Deploy commit 00b68144e583f20a6b67e29cf01bc07f57979ffb or any subsequent commit from the upstream repository to affected smallweb instances.
  • Audit all Jinja2 templates in the project for additional uses of the |safe filter on request-derived data and remove or replace them.
  • Restrict or normalize inbound query strings at the reverse proxy so raw unencoded quote characters are rejected before reaching the Python application.

Patch Information

The official fix is upstream commit 00b68144e583f20a6b67e29cf01bc07f57979ffb, which removes the |safe filter from the qs variable in flag_panel.html and mobile_more.html. Because kagisearch/smallweb uses a rolling release model, no version tag is associated with the fix; operators must track the Git commit hash. Additional context is available in GitHub Issue #854 and VulDB CVE-2026-90583.

Workarounds

  • If patching is not immediately possible, add a WAF or reverse-proxy rule that rejects HTTP requests whose query string contains raw ", <, or > characters.
  • Set a strict Content Security Policy (CSP) that disallows inline scripts and untrusted script sources to reduce impact of any injected payload.
  • Locally override the affected templates to render {{ qs|e }} (explicit escape) until the upstream commit is deployed.
bash
# Example nginx rule to drop requests with raw quote characters in the query string
if ($query_string ~* "[\"<>]") {
    return 400;
}

# Recommended CSP header to mitigate residual XSS risk
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'";

Disclaimer: This content was generated using AI. While we strive for accuracy, please verify critical information with official sources.

Experience the Most Advanced Cybersecurity Platform

See how the world’s most intelligent, autonomous cybersecurity platform can protect your organization today and into the future.