diff --git a/SCAN-TODO.md b/SCAN-TODO.md index e61b9e82e..d16864327 100644 --- a/SCAN-TODO.md +++ b/SCAN-TODO.md @@ -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) diff --git a/defects/dnsdbq-0001/TICKET.md b/defects/dnsdbq-0001/TICKET.md new file mode 100644 index 000000000..84ffeb561 --- /dev/null +++ b/defects/dnsdbq-0001/TICKET.md @@ -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 `, `circla `, +`deteque_t `, and `deteque_a ` 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 diff --git a/defects/dnsdbq-0001/patch/dnsdbq-0001.patch b/defects/dnsdbq-0001/patch/dnsdbq-0001.patch new file mode 100644 index 000000000..cbda831c7 --- /dev/null +++ b/defects/dnsdbq-0001/patch/dnsdbq-0001.patch @@ -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"); + } diff --git a/defects/dnsdbq-0001/test/test_dnsdbq_0001.py b/defects/dnsdbq-0001/test/test_dnsdbq_0001.py new file mode 100644 index 000000000..f19c4cfa7 --- /dev/null +++ b/defects/dnsdbq-0001/test/test_dnsdbq_0001.py @@ -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)