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:
parent
b26dcb053a
commit
e123ca2fc8
4 changed files with 311 additions and 0 deletions
|
|
@ -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)
|
||||
|
|
|
|||
79
defects/dnsdbq-0001/TICKET.md
Normal file
79
defects/dnsdbq-0001/TICKET.md
Normal 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
|
||||
42
defects/dnsdbq-0001/patch/dnsdbq-0001.patch
Normal file
42
defects/dnsdbq-0001/patch/dnsdbq-0001.patch
Normal 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");
|
||||
}
|
||||
189
defects/dnsdbq-0001/test/test_dnsdbq_0001.py
Normal file
189
defects/dnsdbq-0001/test/test_dnsdbq_0001.py
Normal 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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue