Back to Blog

Security Advisory: Header Trust Issues in SearXNG (Open Redirect, Cache Poisoning, Rate-Limiter Bypass)

BackBox AI surfaced two header-trust weaknesses in SearXNG: a Host header injection leading to open redirect and cache poisoning, and an X-Forwarded-For rate-limiter bypass. Disclosed responsibly, both come down to trusting client-controlled HTTP headers.

Security Advisory: Header Trust Issues in SearXNG

This advisory comes out of our open source partnership program, which we introduced in Smarter, Not Bigger: How We Built BackBox AI, and How We Partner with Open Source. The goal of the program is simple: point BackBox AI at widely used open source projects, and feed anything it finds back to the maintainers through responsible disclosure.

This advisory looks at SearXNG, the privacy-respecting metasearch engine. BackBox AI identified two distinct weaknesses that share a single root cause: the application trusts HTTP headers that a client fully controls. On their own each is a medium-severity issue, but together they make a good case study in why perimeter assumptions belong in configuration, not in application logic.

Advisory Summary

Field Finding A Finding B
Component searx/webapp.py (URL generation) searx/botdetection/ (client IP derivation)
Class Host header injection, open redirect, cache poisoning Rate-limiter bypass via IP spoofing
CWE CWE-601 CWE-346, CWE-290
CVSS 3.1 5.4 (AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N) 6.5 (AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:L)
Severity Medium Medium
Precondition Fronting proxy does not pin or overwrite the Host header trusted_proxies unset while X-Forwarded-For is honored
Advisory ID BBL-2026-06031 (internal tracking) BBL-2026-06031 (internal tracking)
Status Reported via responsible disclosure Reported via responsible disclosure

Finding A: Host Header Injection to Open Redirect and Cache Poisoning

SearXNG builds several absolute URLs with Flask's url_for() and _external=True. When Flask constructs an external URL it derives the scheme and authority from the incoming request, which means the value of the client-supplied Host header ends up inside the generated link. In webapp.py this pattern appears in a few security-relevant places:

  • custom_url_for() calls url_for("index") to compute the application prefix (around line 465).
  • preferences() returns make_response(redirect(url_for("index", _external=True))) (around line 735).
  • The preferences save path performs the same redirect (around line 897).
  • clear_cookies() performs the same redirect (around line 1247).

Because the redirect target is derived from the Host header, an attacker who can reach the instance directly (or through a proxy that forwards the header unchanged) can point those redirects at an arbitrary domain.

Reproduction

A GET request to /clear_cookies with a spoofed Host reflects into the Location header:

curl -sv -H "Host: evil.com" "http://TARGET:8080/clear_cookies"
HTTP/1.1 302 FOUND
location: http://evil.com/

The preferences save endpoint is the more interesting one, because it also issues cookies with a five-year lifetime alongside the redirect:

curl -sv -X POST -H "Host: evil.com" "http://TARGET:8080/preferences" -d "language=en"
HTTP/1.1 302 FOUND
location: http://evil.com/
set-cookie: categories=; Expires=Mon, 02 Jun 2031 10:10:46 GMT; Max-Age=157680000; Path=/
set-cookie: language=en; ...

Why It Matters

Two impacts stack here:

  • Phishing via open redirect. A link that begins with the legitimate SearXNG origin, but lands on an attacker domain, may bypass or weaken URL-based reputation checks that rely primarily on the initial trusted domain.
  • Cache poisoning. The 302 responses carry no Cache-Control, Vary, or Pragma headers. If a caching reverse proxy or CDN is configured to cache redirect responses, a poisoned redirect may be stored and subsequently served to legitimate users until the cache entry expires. The long-lived cookies make the poisoned response persistently relevant to the user's session, increasing the practical impact if cached redirects are repeatedly served.

During analysis we also observed that, in the tested deployment, the /config endpoint was reachable and not excluded in robots.txt, making deployment metadata easier to enumerate.

Finding B: Rate-Limiter Bypass via X-Forwarded-For

SearXNG's bot-detection subsystem resolves the client IP from the X-Forwarded-For header. The ProxyFix behavior in searx/botdetection/trusted_proxies.py treats the header as authoritative when trusted_proxies is empty or unset. When the limiter is enabled (server.limiter: true), IP-based rate limiting keys off this derived address, so rotating the header value on every request makes each request look like a brand new client.

Reproducing the Bypass

The IP that SearXNG attributes to the request follows the header verbatim:

# With a spoofed header
curl -s "http://TARGET:8080/search?q=ip&format=json" -H "X-Forwarded-For: 1.2.3.4"
# {"answers": [{"answer": "Your IP is: 1.2.3.4"}]}

# Without it, the real client IP is used
curl -s "http://TARGET:8080/search?q=ip&format=json"
# {"answers": [{"answer": "Your IP is: 172.17.0.1"}]}

Why the Bypass Matters

With per-IP limits defeated, an attacker can effectively evade the per-IP rate limits through the instance. That translates into exhausting the API quotas and keys of the upstream engines SearXNG proxies to, turning the instance into an unrestricted scraping relay, and potentially causing denial of service against those upstream accounts. Independently of the limiter, the same spoofing feeds a fabricated address into the self_info plugin, which is a small but real building block for social-engineering scenarios (for example, screenshots showing a chosen IP).

The Common Root Cause

Both findings reduce to the same assumption: that inbound HTTP headers reflect reality. Host and X-Forwarded-For are set by whoever makes the request. They are trustworthy only when a correctly configured reverse proxy overwrites them before they reach the application, and SearXNG's own documentation is explicit about configuring trusted_proxies for exactly this reason. The weaknesses surface in deployments where that fronting layer is absent or permissive, which is common enough to be worth calling out.

Mitigation

The remediation is primarily operational hardening, and it lines up with SearXNG's documented deployment guidance:

  • For the header injection: avoid deriving security-sensitive external URLs directly from unvalidated request metadata. Pin the expected hostname (for example via Flask's SERVER_NAME or a configured base_url), maintain an allow-list of valid hostnames, and ensure the reverse proxy validates and overwrites the Host header before forwarding. Adding Cache-Control/Vary headers to redirect responses removes the cache-poisoning amplifier.
  • For the limiter bypass: always set botdetection.trusted_proxies to the real reverse-proxy addresses, reject X-Forwarded-For from untrusted sources, and keep the limiter enabled for any internet-facing instance. Signaling in self_info when an address comes from a proxy header rather than a direct connection would make the derivation transparent.

How BackBox AI Found It

Neither finding is an exotic memory-corruption bug; both are logic issues that only stand out when you follow where untrusted input flows. That is exactly the kind of reasoning our architecture is built around, as we discussed in the partnership write-up. BackBox AI traced client-controlled headers from the request boundary through url_for() and the bot-detection IP resolution, recognized that the security of those code paths depends entirely on the reverse proxy enforcing the expected trust boundary, and then confirmed the behavior with the simple curl probes reproduced above. The value was not in generating a payload, but in reasoning about trust boundaries across the request lifecycle.

Disclosure and Status

Both issues were reported to the SearXNG maintainers through responsible disclosure. These are configuration-dependent behaviors rather than clear-cut code defects, and their primary remediation is deployment hardening that mirrors the project's existing guidance. At the time of writing, no upstream code change or CVE has been associated with these findings, which is why we track the advisory internally as BBL-2026-06031; that identifier is a placeholder and will be replaced if and when a CVE is issued.

Conclusion

These findings reinforce a familiar lesson: even mature, security-conscious projects can expose unexpected behavior when deployment assumptions are violated. In both cases, the application behaved as designed, but information treated as trusted crossed a trust boundary that should have been enforced by the reverse proxy.

For operators, the takeaway is straightforward: ensure that security-sensitive headers are generated and sanitized exclusively by trusted infrastructure, and verify those assumptions as part of deployment reviews. Small configuration details can have a disproportionate impact on the security posture of an otherwise robust application.

If you maintain an open source project and think an extra set of eyes would help, apply to partner with us.