linapple: 1 CWE-312 defect (MOAD-0004), MOAD 0001/0002/0003/0005 CLEAN
95 lines
3.6 KiB
Python
95 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Unit test for linapple-0001: FTP user:password credential logged verbatim (CWE-312 / MOAD-0004)
|
|
|
|
LoadConfiguration() in src/Applewin.cpp reads g_sFTPUserPass (format: "user:password")
|
|
from config and immediately prints it verbatim via printf to stdout at every startup.
|
|
|
|
Fix: extract only the username portion; mask the password with ***.
|
|
"""
|
|
|
|
import re
|
|
import unittest
|
|
|
|
# Original defective line
|
|
ORIGINAL_LINE = 'printf("Ready login = %s\\n", g_sFTPUserPass);'
|
|
|
|
# Patched lines (the masked version)
|
|
PATCHED_FMT = 'printf("Ready login = %.*s:***\\n", user_len, g_sFTPUserPass);'
|
|
|
|
|
|
def mask_userpass(userpass: str) -> str:
|
|
"""Simulate the patched masking logic in C."""
|
|
colon = userpass.find(':')
|
|
if colon >= 0:
|
|
return userpass[:colon] + ':***'
|
|
return userpass + ':***'
|
|
|
|
|
|
def original_leak(userpass: str) -> str:
|
|
"""Simulate the original defective print."""
|
|
return userpass
|
|
|
|
|
|
class TestLinapple0001(unittest.TestCase):
|
|
"""Verify that FTP password is not logged in plaintext."""
|
|
|
|
def test_original_exposes_full_credential(self):
|
|
"""Original printf prints user:password verbatim."""
|
|
cred = "alice:s3cr3tpass"
|
|
output = original_leak(cred)
|
|
self.assertIn("s3cr3tpass", output,
|
|
"Defect: password visible in original output")
|
|
# Confirm the source line uses %s with full g_sFTPUserPass
|
|
self.assertIn("%s", ORIGINAL_LINE)
|
|
self.assertIn("g_sFTPUserPass", ORIGINAL_LINE)
|
|
|
|
def test_patch_masks_password(self):
|
|
"""Patched version must show only username, not password."""
|
|
cred = "alice:s3cr3tpass"
|
|
output = mask_userpass(cred)
|
|
self.assertEqual(output, "alice:***",
|
|
"Fix: output should be user:***")
|
|
self.assertNotIn("s3cr3tpass", output,
|
|
"Fix: password must not appear in output")
|
|
|
|
def test_patch_preserves_username(self):
|
|
"""Username must remain visible for diagnostics."""
|
|
cred = "bob:hunter2"
|
|
output = mask_userpass(cred)
|
|
self.assertTrue(output.startswith("bob:"),
|
|
"Fix: username should be preserved")
|
|
|
|
def test_patch_handles_no_colon(self):
|
|
"""If no colon, treat entire string as username."""
|
|
cred = "anonymous"
|
|
output = mask_userpass(cred)
|
|
self.assertIn("anonymous", output)
|
|
self.assertIn("***", output)
|
|
|
|
def test_patch_handles_anonymous_default(self):
|
|
"""Default anonymous:mymail@hotmail.com must have email masked."""
|
|
cred = "anonymous:mymail@hotmail.com"
|
|
output = mask_userpass(cred)
|
|
self.assertNotIn("mymail@hotmail.com", output,
|
|
"Fix: email used as password must not appear in output")
|
|
self.assertEqual(output, "anonymous:***")
|
|
|
|
def test_patched_fmt_string_uses_precision(self):
|
|
"""Patched printf uses %%.*s to limit output to username length."""
|
|
self.assertIn("%.*s", PATCHED_FMT,
|
|
"Patched line should use %%.*s for length-limited username")
|
|
self.assertIn(":***", PATCHED_FMT,
|
|
"Patched line should contain literal :*** mask")
|
|
|
|
def test_original_format_string_leaks(self):
|
|
"""Original format string has single %%s — prints entire user:password."""
|
|
fmt_match = re.search(r'"Ready login = ([^"]*)"', ORIGINAL_LINE)
|
|
self.assertIsNotNone(fmt_match)
|
|
fmt_str = fmt_match.group(1)
|
|
self.assertEqual(fmt_str.count("%s"), 1,
|
|
"Original has one %%s which prints entire credential string")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|