You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
web_search::is_local_base_url gates web_search.allow_remote_base_url: with
the opt-in off, every household search query must stay on-device. It decides
that by prefix-matching the URL text, so it is wrong in both directions.
Remote hosts classified as local — queries leave the device with the opt-in
still off:
base_url
is_local_base_url
actual host
http://localhost.attacker.example/search
true
localhost.attacker.example
http://localhost-evil.example
true
localhost-evil.example
http://127.evil.example/
true
127.evil.example
https://127.0.0.1.nip.io/
true
127.0.0.1.nip.io
http://127.0.0.1@evil.com/search
true
evil.com — the loopback text is userinfo
Genuine loopback URLs classified as remote — a legitimate on-device SearXNG
behind credentials cannot be used at all:
base_url
is_local_base_url
http://user@localhost:8888
false
http://user:pass@[::1]:8888
false
All of the above are observed output from calling the function directly.
Why it matters
This is the guard that keeps household search queries on-device when the user
has not opted into a remote SearXNG. A base_url that merely starts with
loopback-looking text sends every query to a third party, silently, with allow_remote_base_url = false. The userinfo case is the sharp one: the string
looks unambiguously local and the connection goes to evil.com.
Root cause
Prefix matching on the URL string instead of resolving the host. The same
codebase already knows this:
security::sandbox::is_loopback_host (same crate) documents itself as
matching "a literal loopback target (not a hostname that merely starts with
a loopback-looking prefix)", and extract_host beside it strips userinfo
and handles bracketed IPv6.
Make security::sandbox::extract_host / is_loopback_hostpub(crate) and
have is_local_base_url delegate, so this becomes one fewer independent copy
of the decision rather than one more. Behavior for the configurations already
covered by tests is unchanged (127.0.0.1, localhost, [::1] local; searx.example.com remote).
Acceptance
Hosts that merely look loopback are classified remote and blocked
Userinfo-masked remote hosts are classified remote
Loopback behind userinfo, the whole 127.0.0.0/8 range, and mixed-case hosts
are classified local
Focused regression tests fail on main, pass with the fix
Summary
web_search::is_local_base_urlgatesweb_search.allow_remote_base_url: withthe opt-in off, every household search query must stay on-device. It decides
that by prefix-matching the URL text, so it is wrong in both directions.
Repro (on main)
Remote hosts classified as local — queries leave the device with the opt-in
still off:
is_local_base_urlhttp://localhost.attacker.example/searchtruelocalhost.attacker.examplehttp://localhost-evil.exampletruelocalhost-evil.examplehttp://127.evil.example/true127.evil.examplehttps://127.0.0.1.nip.io/true127.0.0.1.nip.iohttp://127.0.0.1@evil.com/searchtrueevil.com— the loopback text is userinfoGenuine loopback URLs classified as remote — a legitimate on-device SearXNG
behind credentials cannot be used at all:
is_local_base_urlhttp://user@localhost:8888falsehttp://user:pass@[::1]:8888falseAll of the above are observed output from calling the function directly.
Why it matters
This is the guard that keeps household search queries on-device when the user
has not opted into a remote SearXNG. A
base_urlthat merely starts withloopback-looking text sends every query to a third party, silently, with
allow_remote_base_url = false. The userinfo case is the sharp one: the stringlooks unambiguously local and the connection goes to
evil.com.Root cause
Prefix matching on the URL string instead of resolving the host. The same
codebase already knows this:
security::sandbox::is_loopback_host(same crate) documents itself asmatching "a literal loopback target (not a hostname that merely starts with
a loopback-looking prefix)", and
extract_hostbeside it strips userinfoand handles bracketed IPv6.
genie-common::config::is_remote_urlhardened its own copy of this decisionafter the class regressed once already ([bug] llm/provider: remote_url() blocks 127.0.0.0/8 except 127.0.0.1 #327,
127.0.0.0/8), and its testcomment explicitly names the genie-core check as the drifted sibling.
is_local_base_urlis the remaining weak copy.Proposed fix
Make
security::sandbox::extract_host/is_loopback_hostpub(crate)andhave
is_local_base_urldelegate, so this becomes one fewer independent copyof the decision rather than one more. Behavior for the configurations already
covered by tests is unchanged (
127.0.0.1,localhost,[::1]local;searx.example.comremote).Acceptance
127.0.0.0/8range, and mixed-case hostsare classified local
Labels / scope
Privacy / egress guard (web search).