clamav: 5-MOAD scan; clamav-0001 CWE-312 proxy password logged verbatim on curl failure
MOAD-0001 (CWE-407): CLEAN. AC trie, BM hash, and hash tables used throughout our hot scan paths. No linear membership scan in our critical code paths. MOAD-0002 (Intertangle): CLEAN. cl_engine is read-only during scan; cli_ctx is stack-local per scan_common invocation. MOAD-0003 (Leaked Context): CLEAN. No pthread thread-locals for scan identity; cli_ctx is per-scan-invocation stack variable. MOAD-0004 (Logged Secret): clamav-0001 — create_curl_handle() in libfreshclam/libfreshclam_internal.c:735 logs g_proxyPassword verbatim in our LOGG_ERROR path when curl_easy_setopt(CURLOPT_PROXYPASSWORD) fails. FreshClam logs are world-readable by default on Linux. Fix: remove our credential from our error message, retain our option name for debugging. MOAD-0005 (Thundering Herd): CLEAN. cache.c uses splay-tree-per-bucket with pthread_mutex_t protecting each bucket on every access.
This commit is contained in:
parent
5e1c00c890
commit
01b9562d57
4 changed files with 267 additions and 0 deletions
|
|
@ -8,6 +8,7 @@ Rule: clone, scan, delete clone after. Keep disk under 90%.
|
|||
- [ ] Squid (C, HTTP proxy, huge install base)
|
||||
- [ ] PgBouncer (C, PostgreSQL connection pooler)
|
||||
- [x] Suricata (C, IDS/IPS) — suricata-0001 (CWE-407 threshold SID lookup O(T×S), CWE-312 auth header logging); suricata-0002 (CWE-407 EveHttpLogJSONHeaders O(H×F=53) per tx, 53x); MOADs 0003/0005 CLEAN
|
||||
- [x] ClamAV (C, antivirus engine) — clamav-0001 MOAD-0004 CWE-312 proxy password logged verbatim on curl failure; MOAD-0001/0002/0003/0005 CLEAN (AC trie, BM hash, mutex-protected cache)
|
||||
- [ ] Snort (C, IDS/IPS)
|
||||
- [ ] WireGuard (deeper, Go userspace tools)
|
||||
- [ ] Thunderbird (C++, email client, undo/history)
|
||||
|
|
|
|||
71
defects/clamav-0001/TICKET.md
Normal file
71
defects/clamav-0001/TICKET.md
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# clamav-0001 — CWE-312 Proxy password logged verbatim on curl setup failure
|
||||
|
||||
**Target:** ClamAV (Cisco-Talos/clamav, current HEAD)
|
||||
**File:** `libfreshclam/libfreshclam_internal.c`
|
||||
**Function:** `create_curl_handle`
|
||||
**Severity:** MEDIUM
|
||||
**MOAD:** 0004 (CWE-312 — Cleartext Storage/Logging of Sensitive Information)
|
||||
**Impact:** Proxy authentication password exposed in log output on curl failure
|
||||
|
||||
## Defect
|
||||
|
||||
When freshclam initializes a libcurl handle with proxy credentials, it logs
|
||||
the raw password string in the error path if `curl_easy_setopt` fails:
|
||||
|
||||
```c
|
||||
// libfreshclam/libfreshclam_internal.c:734-735
|
||||
if (CURLE_OK != curl_easy_setopt(curl, CURLOPT_PROXYPASSWORD, g_proxyPassword)) {
|
||||
logg(LOGG_ERROR, "create_curl_handle: Failed to set CURLOPT_PROXYPASSWORD (%s)!\n", g_proxyPassword);
|
||||
}
|
||||
```
|
||||
|
||||
`g_proxyPassword` is a global holding our proxy authentication password, read
|
||||
from the FreshClam configuration file. If `curl_easy_setopt` fails (for
|
||||
example, if the curl handle is corrupted or the option is unsupported in our
|
||||
libcurl build), our password is emitted to `freshclam.log` and to stderr.
|
||||
|
||||
FreshClam logs are world-readable by default on many Linux distributions
|
||||
(`/var/log/clamav/freshclam.log`, mode 0644). Any local user can read them.
|
||||
|
||||
## Fix
|
||||
|
||||
Remove `%s` and `g_proxyPassword` from our error log message. Our error log
|
||||
should confirm that the option-set operation failed, not echo back our
|
||||
credential:
|
||||
|
||||
```c
|
||||
// Fixed: password redacted from error message
|
||||
if (CURLE_OK != curl_easy_setopt(curl, CURLOPT_PROXYPASSWORD, g_proxyPassword)) {
|
||||
logg(LOGG_ERROR, "create_curl_handle: Failed to set CURLOPT_PROXYPASSWORD!\n");
|
||||
}
|
||||
```
|
||||
|
||||
Same fix applies to CURLOPT_PROXYUSERNAME on line 732, which logs our proxy
|
||||
username (not a password, but still potentially sensitive identity information).
|
||||
|
||||
## MOAD 0001-0005 Scan Results
|
||||
|
||||
- **MOAD-0001 (CWE-407):** CLEAN. Our hot scan paths use the Aho-Corasick
|
||||
trie (O(N) per byte stream), Boyer-Moore hash lookup (O(1) per block), and
|
||||
hash tables throughout. No linear membership scan confirmed in any hot path.
|
||||
Minor bounded O(N*256) in icon group dedup during DB load (G capped at 256).
|
||||
Minor O(N^2) in CRB cert dedup during DB load but CRB files are tiny (<200
|
||||
entries) and loaded once at startup.
|
||||
|
||||
- **MOAD-0002 (Intertangle):** CLEAN. `cl_engine` is treated as immutable
|
||||
during scan. Per-scan state is isolated in `cli_ctx` (stack-local in
|
||||
`scan_common`). The engine is compiled once by `cl_engine_compile`, then
|
||||
read-only for all scan threads.
|
||||
|
||||
- **MOAD-0003 (Leaked Context):** CLEAN. No `pthread_getspecific`,
|
||||
`pthread_setspecific`, or `__thread` thread-locals found in scan paths.
|
||||
`cli_ctx` is allocated per-scan-invocation as a stack variable, not as a
|
||||
thread-scoped carrier.
|
||||
|
||||
- **MOAD-0004 (Logged Secret):** **DEFECT FOUND** — see above.
|
||||
`g_proxyPassword` logged verbatim on curl setup failure.
|
||||
|
||||
- **MOAD-0005 (Thundering Herd):** CLEAN. `libclamav/cache.c` uses a
|
||||
splay-tree-per-bucket design with `pthread_mutex_t` protecting each bucket.
|
||||
Cache lookup and insert are always mutex-guarded (`pthread_mutex_lock` at
|
||||
line 549, unlocked at 559).
|
||||
14
defects/clamav-0001/patch/clamav-0001.patch
Normal file
14
defects/clamav-0001/patch/clamav-0001.patch
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
--- a/libfreshclam/libfreshclam_internal.c
|
||||
+++ b/libfreshclam/libfreshclam_internal.c
|
||||
@@ -729,9 +729,9 @@ static CURL *create_curl_handle(const char *server, bool bCheckCert)
|
||||
if (g_proxyUsername) {
|
||||
if (CURLE_OK != curl_easy_setopt(curl, CURLOPT_PROXYUSERNAME, g_proxyUsername)) {
|
||||
- logg(LOGG_ERROR, "create_curl_handle: Failed to set CURLOPT_PROXYUSERNAME (%s)!\n", g_proxyUsername);
|
||||
+ logg(LOGG_ERROR, "create_curl_handle: Failed to set CURLOPT_PROXYUSERNAME!\n");
|
||||
}
|
||||
if (CURLE_OK != curl_easy_setopt(curl, CURLOPT_PROXYPASSWORD, g_proxyPassword)) {
|
||||
- logg(LOGG_ERROR, "create_curl_handle: Failed to set CURLOPT_PROXYPASSWORD (%s)!\n", g_proxyPassword);
|
||||
+ logg(LOGG_ERROR, "create_curl_handle: Failed to set CURLOPT_PROXYPASSWORD!\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
181
defects/clamav-0001/test/test_clamav_0001.py
Normal file
181
defects/clamav-0001/test/test_clamav_0001.py
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
"""
|
||||
test_clamav_0001.py — CWE-312 proxy password logged verbatim
|
||||
|
||||
ClamAV freshclam's create_curl_handle() logs g_proxyPassword verbatim in our
|
||||
error path when curl_easy_setopt(CURLOPT_PROXYPASSWORD) fails.
|
||||
|
||||
This test simulates our defect and our fix by checking whether a password
|
||||
value appears in a log message produced when a credential setup fails. We
|
||||
measure string-match work done in each case and assert that our fixed version
|
||||
does not expose our credential.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import time
|
||||
import re
|
||||
|
||||
export_PYTHONUNBUFFERED = 1 # reminder: run with PYTHONUNBUFFERED=1
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Simulate defect: log helper that DOES include our credential value
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def log_proxy_password_defect(password: str) -> str:
|
||||
"""Simulate our defective log line that includes our proxy password."""
|
||||
return f"create_curl_handle: Failed to set CURLOPT_PROXYPASSWORD ({password})!\n"
|
||||
|
||||
|
||||
def log_proxy_password_fixed(password: str) -> str:
|
||||
"""Simulate our fixed log line that omits our proxy password."""
|
||||
_ = password # credential is consumed by curl, never reaches our log
|
||||
return "create_curl_handle: Failed to set CURLOPT_PROXYPASSWORD!\n"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Check: does our credential appear in the logged string?
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def credential_visible_in_log(log_line: str, credential: str) -> bool:
|
||||
"""Return True if our credential value is present in our log output."""
|
||||
return credential in log_line
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Performance simulation: at N=100 and N=1000 log entries, measure work
|
||||
# for a log scrubber that must find and redact secrets post-hoc
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def scrub_log_entries_linear(entries: list, credential: str) -> int:
|
||||
"""
|
||||
Simulate our naive post-hoc log scrubber: scan every log entry for our
|
||||
credential string. This is O(N * L) where N = entries, L = line length.
|
||||
Returns count of lines that needed redaction.
|
||||
"""
|
||||
found = 0
|
||||
for entry in entries:
|
||||
if credential in entry:
|
||||
found += 1
|
||||
return found
|
||||
|
||||
|
||||
def scrub_log_entries_set(entries: list, credential: str) -> int:
|
||||
"""
|
||||
Optimized scrubber using a set of known secrets for O(1) membership.
|
||||
Still O(N) over entries but avoids per-char scanning for each secret.
|
||||
Returns count of lines that needed redaction.
|
||||
"""
|
||||
secret_set = {credential}
|
||||
found = 0
|
||||
for entry in entries:
|
||||
# In practice this would use a compiled regex or Aho-Corasick.
|
||||
# Here we simulate O(1) set lookup after extracting token.
|
||||
for secret in secret_set:
|
||||
if secret in entry:
|
||||
found += 1
|
||||
break
|
||||
return found
|
||||
|
||||
|
||||
def benchmark(n: int, credential: str) -> tuple:
|
||||
"""
|
||||
Build N log entries (half defective, half fixed), run both scrubbers.
|
||||
Returns (defective_time_ms, fixed_time_ms, ratio).
|
||||
"""
|
||||
defective_entries = [log_proxy_password_defect(credential) for _ in range(n // 2)]
|
||||
fixed_entries = [log_proxy_password_fixed(credential) for _ in range(n - n // 2)]
|
||||
all_entries = defective_entries + fixed_entries
|
||||
|
||||
# Defect scenario: scrubber must scan every line
|
||||
t0 = time.perf_counter()
|
||||
for _ in range(100):
|
||||
scrub_log_entries_linear(all_entries, credential)
|
||||
defective_time = (time.perf_counter() - t0) * 1000 / 100
|
||||
|
||||
# Fixed scenario: no credentials in log, scrubber finds nothing
|
||||
# (representative of O(N) scan with no matches = fast path)
|
||||
clean_entries = [log_proxy_password_fixed(credential) for _ in range(n)]
|
||||
t1 = time.perf_counter()
|
||||
for _ in range(100):
|
||||
scrub_log_entries_linear(clean_entries, credential)
|
||||
fixed_time = (time.perf_counter() - t1) * 1000 / 100
|
||||
|
||||
# Ratio: how much more work the defective case creates per scan
|
||||
ratio = defective_time / fixed_time if fixed_time > 0 else float("inf")
|
||||
return defective_time, fixed_time, ratio
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Main test
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
password = "s3cr3tProxyP@ssw0rd!"
|
||||
all_passed = True
|
||||
|
||||
print("=== clamav-0001: CWE-312 Proxy password in log ===\n")
|
||||
|
||||
# --- Correctness checks ---
|
||||
|
||||
defect_line = log_proxy_password_defect(password)
|
||||
fixed_line = log_proxy_password_fixed(password)
|
||||
|
||||
# 1. Defect: password IS in our log output
|
||||
result = credential_visible_in_log(defect_line, password)
|
||||
status = "PASS" if result else "FAIL"
|
||||
print(f"[{status}] Defect: password visible in log = {result}")
|
||||
if not result:
|
||||
all_passed = False
|
||||
|
||||
# 2. Fix: password NOT in our log output
|
||||
result = not credential_visible_in_log(fixed_line, password)
|
||||
status = "PASS" if result else "FAIL"
|
||||
print(f"[{status}] Fix: password NOT visible in log = {result}")
|
||||
if not result:
|
||||
all_passed = False
|
||||
|
||||
# 3. Fix log still mentions our option name (useful for debugging)
|
||||
result = "CURLOPT_PROXYPASSWORD" in fixed_line
|
||||
status = "PASS" if result else "FAIL"
|
||||
print(f"[{status}] Fix: log still names the option = {result}")
|
||||
if not result:
|
||||
all_passed = False
|
||||
|
||||
# 4. Defect log contains our password as a %s format expansion
|
||||
pattern = re.compile(r"\(([^)]+)\)")
|
||||
match = pattern.search(defect_line)
|
||||
extracted = match.group(1) if match else ""
|
||||
result = extracted == password
|
||||
status = "PASS" if result else "FAIL"
|
||||
print(f"[{status}] Defect: extracted credential from log = '{extracted}' (expected '{password}')")
|
||||
if not result:
|
||||
all_passed = False
|
||||
|
||||
print()
|
||||
|
||||
# --- Performance benchmarks ---
|
||||
|
||||
for n in [100, 1000]:
|
||||
d_ms, f_ms, ratio = benchmark(n, password)
|
||||
print(f"N={n}: defective scrub={d_ms:.3f}ms, fixed (no matches)={f_ms:.3f}ms, ratio={ratio:.2f}x")
|
||||
# The scrubber must do more work on defective logs because it finds matches.
|
||||
# We assert the defective scenario is at least 1.0x (may be similar since both O(N)).
|
||||
if ratio >= 1.0:
|
||||
print(f"[PASS] N={n}: defective log requires >= 1.0x scrubbing work (ratio={ratio:.2f}x)")
|
||||
else:
|
||||
print(f"[FAIL] N={n}: expected defective >= fixed scrub work, got {ratio:.2f}x")
|
||||
all_passed = False
|
||||
|
||||
print()
|
||||
|
||||
# --- Summary ---
|
||||
|
||||
if all_passed:
|
||||
print("ALL PASS")
|
||||
return 0
|
||||
else:
|
||||
print("SOME TESTS FAILED")
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue