aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-09-08 13:57:11 +0200
committerDanilo M. <danix@danix.xyz>2026-09-08 13:57:11 +0200
commit0f5b1e38f448b554b13e6f6189cb8b7fbd22a95e (patch)
treeaf7e3f662353ef06ad2429948a42ddd0467d811a
parentb491b772b9f68c0c61a62597a972b6cc7848466e (diff)
downloadabusectl-0f5b1e38f448b554b13e6f6189cb8b7fbd22a95e.tar.gz
abusectl-0f5b1e38f448b554b13e6f6189cb8b7fbd22a95e.zip
fix: reject non-list relays and empty cases values
An empty cases string resolved to Path("") = cwd, scattering evidence wherever the command happened to run. A string trusted_relays (easy to hand-write without brackets) iterated as characters, failing on '1' with an error naming nothing findable in the file. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KphFXTc2QajxXsHWyvGJ4R
-rw-r--r--abusectl/config.py22
-rw-r--r--tests/test_config.py29
2 files changed, 49 insertions, 2 deletions
diff --git a/abusectl/config.py b/abusectl/config.py
index c7ba870..38f5254 100644
--- a/abusectl/config.py
+++ b/abusectl/config.py
@@ -74,13 +74,31 @@ def load(from_path: pathlib.Path | None = None) -> Config:
general = data.get("general", {})
- trusted_relays = general.get("trusted_relays") or []
+ trusted_relays = general.get("trusted_relays", [])
+ # A string is iterable, so an unbracketed hand-edit like
+ # trusted_relays = "192.0.2.0/24" would otherwise validate its
+ # characters one at a time and fail with an error naming nothing the
+ # user can find in their file. Reject it up front instead.
+ if not isinstance(trusted_relays, (list, tuple)):
+ raise ValueError(
+ f"trusted_relays in {source} must be a list, got "
+ f"{type(trusted_relays).__name__}"
+ )
if not trusted_relays:
raise NotConfigured(f"no trusted_relays in {source}: run `abusectl init`")
for relay in trusted_relays:
+ if not isinstance(relay, str):
+ raise ValueError(
+ f"trusted_relays entry in {source} must be a string, got "
+ f"{type(relay).__name__}: {relay!r}"
+ )
ipaddress.ip_network(relay, strict=False)
- cases = pathlib.Path(general["cases"]).expanduser() if "cases" in general else DEFAULT_CASES
+ # An empty or whitespace-only value is a skipped answer, same as an
+ # absent key: it must not resolve to Path("") and scatter evidence into
+ # whatever directory the command happened to run from.
+ raw_cases = general.get("cases", "")
+ cases = pathlib.Path(raw_cases).expanduser() if raw_cases.strip() else DEFAULT_CASES
return Config(trusted_relays=trusted_relays, cases=cases)
diff --git a/tests/test_config.py b/tests/test_config.py
index c0a9ac4..6fa8619 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -76,6 +76,35 @@ class TestConfig(unittest.TestCase):
config.load(path)
self.assertNotIsInstance(caught.exception, config.NotConfigured)
+ def test_an_empty_cases_value_falls_back_to_the_default(self):
+ # Empty is the same as absent, per this module's own rule: writing
+ # Path("") would put evidence in whatever directory the command
+ # happened to run from.
+ path = self._write(
+ '[general]\ntrusted_relays = ["192.0.2.0/24"]\ncases = ""\n'
+ )
+ self.assertEqual(config.load(path).cases, config.DEFAULT_CASES)
+
+ def test_a_whitespace_cases_value_falls_back_to_the_default(self):
+ path = self._write(
+ '[general]\ntrusted_relays = ["192.0.2.0/24"]\ncases = " "\n'
+ )
+ self.assertEqual(config.load(path).cases, config.DEFAULT_CASES)
+
+ def test_a_string_trusted_relays_is_rejected_clearly(self):
+ # Easy to write by hand without the brackets. Iterating the string
+ # validates single characters and reports an error naming nothing
+ # the user can find in their file.
+ path = self._write('[general]\ntrusted_relays = "192.0.2.0/24"\n')
+ with self.assertRaises(ValueError) as caught:
+ config.load(path)
+ self.assertIn("must be a list", str(caught.exception))
+
+ def test_a_relay_entry_that_is_not_a_string_is_rejected_clearly(self):
+ path = self._write("[general]\ntrusted_relays = [42]\n")
+ with self.assertRaises(ValueError):
+ config.load(path)
+
def test_the_config_path_follows_xdg_config_home(self):
with mock.patch.dict(os.environ, {"XDG_CONFIG_HOME": "/tmp/xdg-probe"}):
self.assertEqual(