spamhaus/dnsdbq: 5-MOAD scan; dnsdbq-0001 CWE-312 API keys logged verbatim in debug mode

rbldnsd (C, DNS blocklist server): CLEAN all 5 MOADs
- MOAD-0001 CWE-407: CLEAN — all query paths use sorted arrays + binary search
  (ds_dnset_find, ds_ip4set_find, ds_ip4tset_find, ds_generic_find all O(log N))
- MOAD-0002 Intertangle: CLEAN — single-threaded, clear separation of zones/datasets
- MOAD-0003 Leaked Context: CLEAN — no thread-local state, no threading at all
- MOAD-0004 Logged Secret: CLEAN — no credential handling in rbldnsd
- MOAD-0005 Thundering Herd: CLEAN — single-threaded, signal-based reload

dnsdbq (C, pDNS CLI tool, github.com/spamhaus/dnsdbq):
- dnsdbq-0001 MOAD-0004 CWE-312 HIGH: API keys and auth credentials logged
  verbatim to stderr when -v (debug) flag is used. Two sites:
  (1) dnsdbq.c:862 logs every raw config file line including
      "apikey <secret>", "circla user:password", "deteque_t token",
      "deteque_a authinfo"
  (2) dnsdbq.c:915 logs DNSDB_API_KEY env var verbatim
  Fix: redact credential values, log key name + [REDACTED]

pdns-logger (C, PowerDNS logging daemon): CLEAN all 5 MOADs
rdap (Go, RDAP client library): CLEAN all 5 MOADs
This commit is contained in:
russell@unturf.com 2026-04-03 13:05:46 -04:00
parent b26dcb053a
commit e123ca2fc8
4 changed files with 311 additions and 0 deletions

View file

@ -67,6 +67,7 @@ Rule: clone, scan, delete clone after. Keep disk under 90%.
## Priority 8 — Networking/Infra
- [x] Spamhaus tooling: rbldnsd (C, DNS blocklist server) CLEAN all 5 MOADs; dnsdbq (C, pDNS CLI) dnsdbq-0001 MOAD-0004 CWE-312 API key logged verbatim in debug mode; pdns-logger CLEAN; rdap (Go library) CLEAN
- [ ] Forgejo (Go, Gitea fork)
- [ ] Woodpecker CI (Go)
- [ ] Drone CI (Go)

View file

@ -0,0 +1,79 @@
# dnsdbq-0001 — CWE-312 API Keys and Credentials Logged Verbatim in Debug Mode
**MOAD:** 0004 — Our Logged Secret
**Severity:** HIGH
**Target:** spamhaus/dnsdbq (DNSDB passive DNS query tool)
**File:** `dnsdbq.c`
## Summary
When invoked with `-v` (verbose/debug) flag, `dnsdbq` logs authentication
credentials verbatim to stderr. Two sites expose secrets:
**Site 1 (line 862):** Every line of our credential config file is echoed
verbatim, including `apikey <secret>`, `circla <user:password>`,
`deteque_t <token>`, and `deteque_a <auth>` values.
```c
if (debuglev > 0)
fprintf(stderr, "conf line: %s", line);
```
This fires for every config line parsed. A config file containing:
```
apikey deadbeef12345678abcdef
circla user:s3cr3tpassword
deteque_t mytoken
deteque_a myauthinfo
```
...will have all values printed to stderr when `-v` is passed.
**Site 2 (line 915-916):** The API key read from the environment variable
`DNSDB_API_KEY` is logged verbatim:
```c
if (debuglev > 0)
fprintf(stderr, "conf env api_key = '%s'\n", api_key);
```
## Impact
- DNSDB API keys grant access to passive DNS data; exposure leaks query
capability and potentially billing/attribution.
- CIRCL authinfo is a `user:password` basic auth credential; full exposure.
- Deteque token/auth are service credentials.
- Any shell session recording, CI log capture, or terminal sharing exposes
all credentials when `-v` is used for debugging.
## Complexity
O(1) per credential, but triggered by debug flag that developers routinely
use. Logs are often captured in CI, shared in bug reports, or sent to
support. Debug output that contains secrets is a systematic disclosure risk.
## Fix
Redact credential values in debug output. Safe alternatives:
```c
// Site 1: instead of logging raw config lines, log only the key name
if (debuglev > 0) {
// Only log the key name (tok1), never the value (tok2) for credential keys
if (strcmp(tok1, "apikey") == 0 || strcmp(tok1, "circla") == 0 ||
strcmp(tok1, "deteque_t") == 0 || strcmp(tok1, "deteque_a") == 0)
fprintf(stderr, "conf line: %s [REDACTED]\n", tok1);
else
fprintf(stderr, "conf line: %s", line);
}
// Site 2: mask the API key
if (debuglev > 0)
fprintf(stderr, "conf env api_key = [REDACTED]\n");
```
## Files
- `patch/dnsdbq-0001.patch` — proposed fix
- `test/test_dnsdbq_0001.py` — simulation + benchmark

View file

@ -0,0 +1,42 @@
--- a/dnsdbq.c
+++ b/dnsdbq.c
@@ -858,8 +858,19 @@ static void
if (debuglev > 0)
fprintf(stderr, "conf cmd = '%s'\n", cmd);
DESTROY(cmd);
line = NULL;
n = 0;
while (getline(&line, &n, f) > 0) {
char **pp;
if (strchr(line, '\n') == NULL) {
fprintf(stderr, "line too long: '%s'\n", line);
my_exit(1, cf, NULL);
}
- if (debuglev > 0)
- fprintf(stderr, "conf line: %s", line);
+ /* CWE-312: never log credential values verbatim.
+ * Log key name only; redact the value for credential keys. */
+ if (debuglev > 0) {
+ char *lp = strdup(line);
+ char *k = strtok(lp, "\040\012");
+ int is_cred = (k != NULL) && (
+ strcmp(k, "apikey") == 0 ||
+ strcmp(k, "circla") == 0 ||
+ strcmp(k, "deteque_t") == 0 ||
+ strcmp(k, "deteque_a") == 0);
+ if (is_cred)
+ fprintf(stderr, "conf line: %s [REDACTED]\n", k ? k : "?");
+ else
+ fprintf(stderr, "conf line: %s", line);
+ free(lp);
+ }
tok1 = strtok(line, "\040\012");
@@ -913,7 +924,8 @@ static void
api_key = strdup(val);
- if (debuglev > 0)
- fprintf(stderr, "conf env api_key = '%s'\n", api_key);
+ /* CWE-312: redact API key in debug output */
+ if (debuglev > 0)
+ fprintf(stderr, "conf env api_key = [REDACTED]\n");
}

View file

@ -0,0 +1,189 @@
"""
dnsdbq-0001 CWE-312 Logged Secret
MOAD-0004: credentials logged verbatim in debug mode.
Simulates the config-file and env-var logging paths in dnsdbq.c.
Demonstrates redacted vs. verbatim output.
Benchmark: N iterations of the logging decision.
Assert speedup > 3x not applicable here (it's a correctness defect, not
a performance defect). Instead: assert redacted output never contains
our secret, and verbatim output does contain it (confirming the defect).
"""
import os
import sys
import time
import io
# ---------------------------------------------------------------------------
# Simulate the defective logging path (dnsdbq.c line 862 verbatim)
# ---------------------------------------------------------------------------
CREDENTIAL_KEYS = {"apikey", "circla", "deteque_t", "deteque_a"}
def log_config_line_defective(line, buf):
"""Verbatim log — reproduces the defect."""
buf.write("conf line: " + line)
def log_config_line_fixed(line, buf):
"""Redact credential values — the fix."""
parts = line.strip().split()
if not parts:
buf.write("conf line: " + line)
return
key = parts[0]
if key in CREDENTIAL_KEYS:
buf.write("conf line: " + key + " [REDACTED]\n")
else:
buf.write("conf line: " + line)
def log_env_api_key_defective(api_key, buf):
"""Verbatim env-key log — reproduces the defect."""
buf.write("conf env api_key = '{}'\n".format(api_key))
def log_env_api_key_fixed(buf):
"""Redacted env-key log — the fix."""
buf.write("conf env api_key = [REDACTED]\n")
# ---------------------------------------------------------------------------
# Test: defect exposes secrets
# ---------------------------------------------------------------------------
def test_defect_exposes_secret():
secret = "deadbeef12345678"
config_lines = [
"apikey " + secret + "\n",
"server https://api.dnsdb.info\n",
"circla user:" + secret + "\n",
"deteque_t mytoken_" + secret + "\n",
"deteque_a auth_" + secret + "\n",
]
buf = io.StringIO()
for line in config_lines:
log_config_line_defective(line, buf)
output = buf.getvalue()
assert secret in output, "FAIL: defect not reproduced — secret not found in defective log"
print("CONFIRMED DEFECT: secret '{}...' appears in defective log output".format(secret[:8]))
# Also test env var path
buf2 = io.StringIO()
log_env_api_key_defective(secret, buf2)
assert secret in buf2.getvalue(), "FAIL: env-var defect not reproduced"
print("CONFIRMED DEFECT: env API key logged verbatim")
# ---------------------------------------------------------------------------
# Test: fix redacts secrets
# ---------------------------------------------------------------------------
def test_fix_redacts_secret():
secret = "deadbeef12345678"
config_lines = [
"apikey " + secret + "\n",
"server https://api.dnsdb.info\n",
"circla user:" + secret + "\n",
"deteque_t mytoken_" + secret + "\n",
"deteque_a auth_" + secret + "\n",
]
buf = io.StringIO()
for line in config_lines:
log_config_line_fixed(line, buf)
output = buf.getvalue()
assert secret not in output, "FAIL: fix failed — secret still appears in fixed log"
assert "[REDACTED]" in output, "FAIL: fix missing REDACTED marker"
# Non-credential lines should still be logged
assert "https://api.dnsdb.info" in output, "FAIL: non-credential line was incorrectly redacted"
print("CONFIRMED FIX: secret not in fixed log output")
print("Fixed output:\n" + output)
# Also test env var fix
buf2 = io.StringIO()
log_env_api_key_fixed(buf2)
assert secret not in buf2.getvalue(), "FAIL: env-var fix failed"
assert "[REDACTED]" in buf2.getvalue(), "FAIL: env-var fix missing REDACTED marker"
print("CONFIRMED FIX: env API key redacted")
# ---------------------------------------------------------------------------
# Benchmark: N=100 and N=1000 iterations of both paths
# ---------------------------------------------------------------------------
def benchmark(N):
secret = "s3cr3t_api_key_value"
config_lines = [
"apikey " + secret + "\n",
"server https://api.dnsdb.info\n",
"circla user:" + secret + "\n",
] * 3 # 9 lines per iteration
# Defective path
t0 = time.perf_counter()
for _ in range(N):
buf = io.StringIO()
for line in config_lines:
log_config_line_defective(line, buf)
t_defective = time.perf_counter() - t0
# Fixed path
t0 = time.perf_counter()
for _ in range(N):
buf = io.StringIO()
for line in config_lines:
log_config_line_fixed(line, buf)
t_fixed = time.perf_counter() - t0
print("N={:4d}: defective={:.3f}ms fixed={:.3f}ms".format(
N,
t_defective * 1000,
t_fixed * 1000
))
# For CWE-312 the fix adds minor overhead (extra split + set lookup).
# The fix is not a speedup over the defect; it is a correctness improvement.
# We assert the overhead is bounded (< 5x slower), not faster.
ratio = t_fixed / t_defective if t_defective > 0 else 1.0
assert ratio < 5.0, "FAIL: fix has unexpectedly high overhead ({:.1f}x)".format(ratio)
return t_defective, t_fixed
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
if __name__ == "__main__":
export_PYTHONUNBUFFERED = True # ensure output is visible
os.environ["PYTHONUNBUFFERED"] = "1"
print("=== dnsdbq-0001 CWE-312 Logged Secret Test ===")
print()
ok = True
try:
test_defect_exposes_secret()
except AssertionError as e:
print("FAIL:", e)
ok = False
try:
test_fix_redacts_secret()
except AssertionError as e:
print("FAIL:", e)
ok = False
print()
print("=== Benchmark ===")
for N in [100, 1000]:
try:
benchmark(N)
except AssertionError as e:
print("FAIL:", e)
ok = False
print()
if ok:
print("PASS")
else:
print("FAIL")
sys.exit(1)