fuse: all 5 MOADs CLEAN
linapple: 1 CWE-312 defect (MOAD-0004), MOAD 0001/0002/0003/0005 CLEAN
This commit is contained in:
parent
4b180cd7ac
commit
20b8a57f89
4 changed files with 186 additions and 0 deletions
33
defects/fuse-0001/SUMMARY.md
Normal file
33
defects/fuse-0001/SUMMARY.md
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# fuse: all 5 MOADs CLEAN
|
||||
|
||||
ZX Spectrum emulator, C codebase. Shallow clone scanned 2026-03-31.
|
||||
|
||||
## MOAD-0001 CWE-407
|
||||
|
||||
CLEAN. Hot paths use GLib GHashTable for all key lookups (O(1)). Port I/O
|
||||
dispatch uses g_slist_foreach with O(P) per port read/write where P is
|
||||
registered ports (bounded, typically <20). Breakpoint check iterates GSList
|
||||
of breakpoints O(B) per instruction only when debugger is ACTIVE mode.
|
||||
Disk sector search is O(S) bounded by sectors-per-track (9-18).
|
||||
No list.contains() inside a loop on unbounded data found.
|
||||
|
||||
## MOAD-0002 Intertangle
|
||||
|
||||
CLEAN. Global GHashTable for peripherals and GSList for ports are expected
|
||||
single-instance emulator design. No evidence of independent subsystems
|
||||
inappropriately coupled through shared mutable state.
|
||||
|
||||
## MOAD-0003 Leaked Context
|
||||
|
||||
CLEAN. No ThreadLocal, pthread_key, or thread-scoped identity carriers found.
|
||||
Fuse is single-threaded.
|
||||
|
||||
## MOAD-0004 CWE-312
|
||||
|
||||
CLEAN. No credentials, API keys, tokens, or passwords found in logging paths.
|
||||
Spectranet peripheral has no credential-bearing network calls in our codebase.
|
||||
|
||||
## MOAD-0005 Thundering Herd
|
||||
|
||||
CLEAN. No unsynchronized cache get+null+compute+put patterns. Single-threaded
|
||||
architecture eliminates this class of defect.
|
||||
44
defects/linapple-0001/SUMMARY.md
Normal file
44
defects/linapple-0001/SUMMARY.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# linapple-0001: FTP user:password logged verbatim at startup (CWE-312 / MOAD-0004)
|
||||
|
||||
**Severity:** HIGH
|
||||
**File:** `src/Applewin.cpp`
|
||||
**Function:** `LoadConfiguration()`
|
||||
**Line:** 871
|
||||
|
||||
## Description
|
||||
|
||||
`LoadConfiguration()` reads `g_sFTPUserPass` (format: `user:password`) from the
|
||||
registry or config file, then immediately prints the entire credential string
|
||||
verbatim to stdout via `printf`:
|
||||
|
||||
```c
|
||||
// Print some debug strings
|
||||
printf("Ready login = %s\n", g_sFTPUserPass);
|
||||
```
|
||||
|
||||
This debug line runs at every startup. Any user who has configured FTP credentials
|
||||
for disk image access (non-anonymous logins) has their password written to
|
||||
the terminal, any log file, shell history replay, or screen recording session.
|
||||
|
||||
## Fix
|
||||
|
||||
Extract only our username portion (up to the first `:`), mask our password with `***`:
|
||||
|
||||
```c
|
||||
{
|
||||
/* CWE-312: g_sFTPUserPass is "user:password" — never log verbatim. */
|
||||
const char *colon = strchr(g_sFTPUserPass, ':');
|
||||
int user_len = colon ? (int)(colon - g_sFTPUserPass) : (int)strlen(g_sFTPUserPass);
|
||||
printf("Ready login = %.*s:***\n", user_len, g_sFTPUserPass);
|
||||
}
|
||||
```
|
||||
|
||||
## MOADs 0001-0005
|
||||
|
||||
| MOAD | Status |
|
||||
|------|--------|
|
||||
| 0001 CWE-407 | CLEAN — no hot-path O(N^2) linear scan found |
|
||||
| 0002 Intertangle | CLEAN — global state coupling is expected single-instance emulator design |
|
||||
| 0003 Leaked Context | CLEAN — no ThreadLocal/thread-scoped identity carriers |
|
||||
| 0004 CWE-312 | **DEFECT** — FTP password logged verbatim at startup |
|
||||
| 0005 Thundering Herd | CLEAN — no unsynchronized cache patterns found |
|
||||
14
defects/linapple-0001/patch/linapple-0001.patch
Normal file
14
defects/linapple-0001/patch/linapple-0001.patch
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# UNDF: UNDF-2026-XXXXXXXXX
|
||||
--- a/src/Applewin.cpp
|
||||
+++ b/src/Applewin.cpp
|
||||
@@ -868,7 +868,12 @@ static void LoadConfiguration(bool registry)
|
||||
}
|
||||
// Print some debug strings
|
||||
- printf("Ready login = %s\n", g_sFTPUserPass);
|
||||
+ {
|
||||
+ /* CWE-312: g_sFTPUserPass is "user:password" — never log verbatim. */
|
||||
+ const char *colon = strchr(g_sFTPUserPass, ':');
|
||||
+ int user_len = colon ? (int)(colon - g_sFTPUserPass) : (int)strlen(g_sFTPUserPass);
|
||||
+ printf("Ready login = %.*s:***\n", user_len, g_sFTPUserPass);
|
||||
+ }
|
||||
}
|
||||
95
defects/linapple-0001/test/test_linapple_0001.py
Normal file
95
defects/linapple-0001/test/test_linapple_0001.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
#!/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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue