java-topology/defects/sendmail-0001/test/test_sendmail_0001.py
russell@unturf.com 0f2062b0cc sendmail: 5-MOAD scan; sendmail-0001 CWE-312 SASL client password logged verbatim at debug level
getauth() in usersmtp.c logs all SASL auth fields including password
verbatim when tTd(95,5) debug flag is active. Fix: redact password
field with <REDACTED> at log serialization layer. 6/6 tests PASS.
MOADs 0001/0002/0003/0005 CLEAN.
2026-04-03 13:17:05 -04:00

171 lines
5.5 KiB
Python

"""
sendmail-0001 — MOAD-0004 (CWE-312)
SASL client password logged verbatim at debug level in getauth()
Simulates our defect (logging a password field verbatim) and our fix
(redacting password field before logging). Asserts our fix never exposes
our plaintext password in our log output.
"""
import sys
import time
export_PYTHONUNBUFFERED = True # reminder: run with python3 -u or PYTHONUNBUFFERED=1
# --- Constants mirroring sendmail's sasl_info_name[] ---
SASL_USER = 0
SASL_AUTHID = 1
SASL_PASSWORD = 2
SASL_DEFREALM = 3
SASL_MECHLIST = 4
sasl_info_name = ["user id", "authentication id", "password", "realm", "mechlist"]
# --- Simulate the SASL auth info array ---
sai = [
"relay_user", # SASL_USER
"relay_authid", # SASL_AUTHID
"s3cr3t_relay_pass", # SASL_PASSWORD
"example.com", # SASL_DEFREALM
"PLAIN LOGIN", # SASL_MECHLIST
]
def getauth_defective(sai):
"""
Simulates our defective getauth() logging path from sendmail/usersmtp.c ~line 1001.
Logs all fields verbatim — password is exposed.
"""
log_lines = []
for r in range(len(sai)):
# tTd(95, 5) debug path — logs name=value verbatim
line = "getauth {}={}".format(sasl_info_name[r], sai[r])
log_lines.append(line)
return log_lines
def getauth_fixed(sai):
"""
Simulates our fixed getauth() logging path.
Redacts our password field before logging.
"""
log_lines = []
for r in range(len(sai)):
value = "<REDACTED>" if r == SASL_PASSWORD else sai[r]
line = "getauth {}={}".format(sasl_info_name[r], value)
log_lines.append(line)
return log_lines
def password_exposed_in_log(log_lines, password):
"""Return True if our plaintext password appears anywhere in our log output."""
return any(password in line for line in log_lines)
def password_field_logged(log_lines):
"""Return True if our password field log line appears (field name present)."""
return any("password" in line for line in log_lines)
def run_tests():
password = sai[SASL_PASSWORD]
passed = 0
failed = 0
print("=" * 60)
print("sendmail-0001 — MOAD-0004 CWE-312 unit tests")
print("=" * 60)
# --- Test 1: Defective version exposes password ---
defect_log = getauth_defective(sai)
exposed = password_exposed_in_log(defect_log, password)
if exposed:
print("PASS test1: defective getauth() exposes plaintext password in log")
passed += 1
else:
print("FAIL test1: defective getauth() did not expose password (unexpected)")
failed += 1
# --- Test 2: Fixed version does NOT expose password ---
fixed_log = getauth_fixed(sai)
exposed_after_fix = password_exposed_in_log(fixed_log, password)
if not exposed_after_fix:
print("PASS test2: fixed getauth() does NOT expose plaintext password in log")
passed += 1
else:
print("FAIL test2: fixed getauth() still exposes plaintext password")
failed += 1
# --- Test 3: Fixed version still logs our password field name (traceability) ---
field_present = password_field_logged(fixed_log)
if field_present:
print("PASS test3: fixed getauth() still logs password field name for traceability")
passed += 1
else:
print("FAIL test3: fixed getauth() removed password field log line entirely")
failed += 1
# --- Test 4: Fixed version logs REDACTED marker ---
redacted_present = any("<REDACTED>" in line for line in fixed_log)
if redacted_present:
print("PASS test4: fixed getauth() logs <REDACTED> marker for password field")
passed += 1
else:
print("FAIL test4: fixed getauth() missing <REDACTED> marker")
failed += 1
# --- Test 5: Fixed version logs non-secret fields unchanged ---
username_logged = any("relay_user" in line for line in fixed_log)
if username_logged:
print("PASS test5: fixed getauth() logs non-secret fields (username) unchanged")
passed += 1
else:
print("FAIL test5: fixed getauth() suppressed non-secret field logging")
failed += 1
# --- Test 6: Benchmark — simulate N=100 and N=1000 SASL auth attempts ---
# Our fix adds one integer comparison per field per auth attempt — negligible overhead.
# We assert our fix's overhead is < 10x relative to a plain strcmp (it's O(1)).
import timeit
N = 100000 # auth attempts to benchmark
def bench_defective():
for _ in range(N):
getauth_defective(sai)
def bench_fixed():
for _ in range(N):
getauth_fixed(sai)
t_defective = timeit.timeit(bench_defective, number=1)
t_fixed = timeit.timeit(bench_fixed, number=1)
ratio = t_fixed / t_defective if t_defective > 0 else 1.0
print()
print("Benchmark ({} iterations):".format(N))
print(" Defective: {:.4f}s".format(t_defective))
print(" Fixed: {:.4f}s".format(t_fixed))
print(" Overhead: {:.2f}x (expected < 2x for ternary check)".format(ratio))
if ratio < 2.0:
print("PASS test6: fix overhead < 2x (single ternary, O(1))")
passed += 1
else:
print("FAIL test6: fix overhead unexpectedly large ({:.2f}x)".format(ratio))
failed += 1
print()
print("=" * 60)
total = passed + failed
print("Results: {}/{} PASS".format(passed, total))
if failed == 0:
print("ALL PASS")
return 0
else:
print("FAILURES: {}".format(failed))
return 1
if __name__ == "__main__":
sys.exit(run_tests())