Repository navigation
[BUG] Two SecurityConfig values silently disable a control while it still reports enabled #79
Description
Both accepted without a warning, both turn something off:
whitelist entries with prefix length 0. A whitelist is checked before the
blacklist, so whitelist=["0.0.0.0/0"] (or ["::/0"]) nullifies the entire
blocklist:
blacklist only -> HTTP 403
blacklist + whitelist ['0.0.0.0/0'] -> HTTP 200
blacklist + whitelist ['::/0'] -> HTTP 200
Plausible as a first attempt at "allow everyone for now" during a rollout, and
it leaves the blocklist in the config, looking enforced.
enabled_detection_categories=frozenset(). Detection reports enabled and
matches nothing:
default categories -> SQLi probe HTTP 400, enable_penetration_detection=True
enabled_detection_categories=frozenset() -> SQLi probe HTTP 200, enable_penetration_detection=True
Reachable through a set comprehension that filters to nothing, which is how
category lists tend to get built from an env var.
Related, same shape: trusted_proxies=["0.0.0.0/0"] is accepted, which makes
every peer trusted to set X-Forwarded-For.
poc_failopen_config.py
"""Two SecurityConfig values that turn a control off without saying so.
uv run python docs/upstream-guard-core/followups/poc_failopen_config.py
"""
import logging
from importlib.metadata import version
logging.getLogger("guard_core").setLevel(logging.CRITICAL)
from fastapi import FastAPI # noqa: E402
from guard.middleware import SecurityMiddleware # noqa: E402
from guard_core.models import SecurityConfig # noqa: E402
from starlette.testclient import TestClient # noqa: E402
def app(**cfg) -> FastAPI:
application = FastAPI()
application.add_middleware(
SecurityMiddleware, config=SecurityConfig(enable_redis=False, **cfg)
)
@application.get("/")
async def root():
return {"ok": True}
return application
print(f"guard-core {version('guard-core')} / fastapi-guard {version('fastapi-guard')}")
print("\n a whitelist entry with prefix length 0 overrides the blacklist")
for label, cfg in (
("blacklist only", {"blacklist": ["66.66.66.0/24"]}),
("blacklist + whitelist ['0.0.0.0/0']", {"blacklist": ["66.66.66.0/24"], "whitelist": ["0.0.0.0/0"]}),
("blacklist + whitelist ['::/0']", {"blacklist": ["66.66.66.0/24"], "whitelist": ["::/0", "0.0.0.0/0"]}),
):
with TestClient(app(**cfg), client=("66.66.66.9", 1)) as client:
print(f" {label:38} -> HTTP {client.get('/').status_code}")
print("\n an empty category set switches detection off while it still reports enabled")
sqli = {"q": "' OR 1=1 --"}
for label, cfg in (
("default categories", {}),
("enabled_detection_categories=frozenset()", {"enabled_detection_categories": frozenset()}),
):
config = SecurityConfig(enable_redis=False, **cfg)
with TestClient(app(**cfg), client=("203.0.113.9", 1)) as client:
status = client.get("/", params=sqli).status_code
print(f" {label:42} -> SQLi probe HTTP {status}, "
f"enable_penetration_detection={config.enable_penetration_detection}")
print("\n neither construction warns:")
import warnings # noqa: E402
for label, cfg in (
("whitelist=['0.0.0.0/0']", {"whitelist": ["0.0.0.0/0"]}),
("enabled_detection_categories=frozenset()", {"enabled_detection_categories": frozenset()}),
("trusted_proxies=['0.0.0.0/0']", {"trusted_proxies": ["0.0.0.0/0"]}),
):
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
SecurityConfig(enable_redis=False, **cfg)
print(f" {label:42} -> {len(caught)} warnings")Fix: warn (or reject) on a whitelist or trusted_proxies entry with prefix
length 0, and on an empty enabled_detection_categories while
enable_penetration_detection is True. A field validator on each is enough —
these are the configurations where a warning at startup is worth more than any
amount of documentation.
Found while re-checking a private audit against the current release. Verified on guard-core 3.13.0, CPython 3.14.
Activity
rennf93 commentedon Aug 25, 2026
Thanks @RomanPszonka for opening the issue! By @rennf93's directive:
Reproduced all three. Rulings I've made:
whitelistfield description anddocs/internals/ip-management.md) and pinned by tests since 3.2.0.0.0.0.0/0is that rule at its logical extreme, and "allow everyone" is what the operator wrote.enabled_detection_categories=frozenset(): the primitive's behaviour (empty set enables nothing) is intended and tested, but the config surface is undocumented and warns nothing. 3.14.0 logs a startup warning when the set is empty whileenable_penetration_detectionisTrue, and documents it.trusted_proxies=["0.0.0.0/0"]: agreed this is the serious one; it is the CVE-2025-46814 shape reached by configuration. 3.14.0 logs a startup warning on a prefix-length-0trusted_proxiesentry.Both are log warnings rather than
warnings.warn, consistent with the private-IP ban warning shipped in a2d8e00.rennf93 commentedon Aug 26, 2026
Shipped in guard-core 3.14.0 (PyPI), merged in #82 (500d4ef).
Part 2:
SecurityConfignow warns at construction on a prefix-0trusted_proxiesentry (0.0.0.0/0,::/0) and on an emptyenabled_detection_categorieswhile detection is enabled. Part 1 (a0.0.0.0/0whitelist taking precedence over the blacklist) stays as designed, per the earlier reply; the precedence is documented and tested.RomanPszonka commentedon Aug 26, 2026
Two of the three landed — the empty-category warning and the
/0check ontrusted_proxiesare both in 3.14.0. Thewhitelisthalf is still open, andit is the one of the three that turns an access control off:
_is_prefix_zero_trusted_proxy_entryis already written and already does theright test; it just is not wired to
whitelist.script
Verified on
guard-core3.14.0 /fastapi-guard7.8.0, CPython 3.14.rennf93 commentedon Aug 26, 2026
Thanks @RomanPszonka, you are right that the whitelist half is the odd one out, and re-reading the thread we conflated two different asks.
What was ruled out on 2026-08-25, and stays ruled out, is a behaviour change: a
/0entry inwhitelistmeans "allow every address", the whitelist takes precedence over the blacklist by design (documented in thewhitelistfield description anddocs/internals/ip-management.md, pinned by tests since 3.2.0), and0.0.0.0/0is that rule at its logical extreme. Refusing or reinterpreting the entry would change what the operator wrote.What your comment asks for is different: a signal, the same one the other two items got in 3.14.0. On that we agree. A
/0whitelist entry silently turns the blacklist and the country rules off, and today it is the only one of the three that warns nothing._is_prefix_zero_trusted_proxy_entryis the right test and is only wired totrusted_proxies.Planned for 3.14.1:
SecurityConfiglogs a warning at construction whenwhitelistcontains a prefix-0 network (0.0.0.0/0or::/0), naming the consequence: every address is whitelisted, soblacklist,blocked_countriesand IP bans cannot block anyone. Same channel as the two 3.14.0 warnings (logger.warningon theguard_corelogger, per construction), sovalidate_configin guard-core-mcp surfaces it as well.200for both whitelist rows; the difference is the warning line.trusted_proxieswarning is documented.This issue stays closed for the 3.14.0 items; the 3.14.1 change will reference it in the changelog.
release(v3.15.0): post-3.14.0 follow-ups (#83)
rennf93 commentedon Aug 27, 2026
Shipped in guard-core 3.15.0 (PyPI), merged in #83 (75d1334).
SecurityConfignow warns at construction whenwhitelistcontains a prefix-0 network (0.0.0.0/0,::/0), naming the consequence: every address is whitelisted, soblacklist,blocked_countriesand IP bans cannot block anyone. Precedence is unchanged; your two whitelist rows still answer 200, now with the warning line. This closes the third of the three signals.rennf93 commentedon Aug 27, 2026
@RomanPszonka 3.15.0 is out (minor, not 3.14.1: one change is labelled breaking). Yours in it: the /0 whitelist warning (this issue), the nested-JSON recursion fix (GHSA-f6cf-jjhc-qp85, published with the release), and the two residuals you called partially fixed: 3hfx now has a per-request character budget on top of the value count plus a 4x cheaper scan, and 8xvm's depth over-count is corrected (entries right of the selected one must be listed proxies, otherwise the chain is walked right to left) instead of only warned. The published 3hfx and 8xvm advisories carry a "Fixed in 3.15.0" section. Thanks again for the precision of the reports.