zesarux: 1 CWE-312 defect, MOAD 0001/0002/0003/0005 CLEAN
yabause: 1 CWE-312 defect, MOAD 0001/0002/0003/0005 CLEAN vita3k-0001: unit test added for pre-existing CWE-407 patch
This commit is contained in:
parent
c7344d5783
commit
2f0da6752a
15 changed files with 741 additions and 0 deletions
55
defects/vita3k-0001/patch/vita3k-0001.patch
Normal file
55
defects/vita3k-0001/patch/vita3k-0001.patch
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# UNDF: UNDF-2026-XXXXXXXXX
|
||||
--- a/vita3k/ngs/src/route.cpp
|
||||
+++ b/vita3k/ngs/src/route.cpp
|
||||
@@ -17,6 +17,8 @@
|
||||
|
||||
#include <ngs/system.h>
|
||||
|
||||
+#include <unordered_set>
|
||||
+
|
||||
#include <util/vector_utils.h>
|
||||
|
||||
namespace ngs {
|
||||
@@ -28,7 +30,13 @@ bool deliver_data(const MemState &mem, const std::vector<Voice *> &voice_queue,
|
||||
if (!patch || patch->output_sub_index == -1)
|
||||
continue;
|
||||
|
||||
- if (!vector_utils::contains(voice_queue, patch->dest))
|
||||
+ if (voice_queue_set.find(patch->dest) == voice_queue_set.end())
|
||||
continue;
|
||||
|
||||
const std::lock_guard<std::mutex> guard(*patch->dest->voice_mutex);
|
||||
--- a/vita3k/ngs/include/ngs/system.h
|
||||
+++ b/vita3k/ngs/include/ngs/system.h
|
||||
@@ -15,6 +15,8 @@
|
||||
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
+#include <unordered_set>
|
||||
+
|
||||
#pragma once
|
||||
|
||||
#include <mem/ptr.h>
|
||||
@@ -218,4 +220,4 @@ struct Rack;
|
||||
|
||||
-bool deliver_data(const MemState &mem, const std::vector<Voice *> &voice_queue, Voice *source, const uint8_t output_port,
|
||||
+bool deliver_data(const MemState &mem, const std::vector<Voice *> &voice_queue, const std::unordered_set<Voice *> &voice_queue_set, Voice *source, const uint8_t output_port,
|
||||
const VoiceProduct &data_to_deliver);
|
||||
--- a/vita3k/ngs/src/scheduler.cpp
|
||||
+++ b/vita3k/ngs/src/scheduler.cpp
|
||||
@@ -120,10 +120,12 @@ void VoiceScheduler::update(KernelState &kern, const MemState &mem, const SceUI
|
||||
// make a copy of the queue, this way we have no issue if it is modified in a callback
|
||||
std::vector<ngs::Voice *> queue_copy = queue;
|
||||
+ const std::unordered_set<ngs::Voice *> queue_set(queue_copy.begin(), queue_copy.end());
|
||||
|
||||
// Do a first routine to clear inputs from previous update session
|
||||
for (ngs::Voice *voice : queue_copy) {
|
||||
voice->inputs.reset_inputs();
|
||||
}
|
||||
|
||||
@@ -160,7 +162,7 @@ void VoiceScheduler::update(KernelState &kern, const MemState &mem, const SceUI
|
||||
for (size_t i = 0; i < voice->rack->vdef->output_count; i++) {
|
||||
if (voice->products[i].data)
|
||||
- deliver_data(mem, queue_copy, voice, static_cast<uint8_t>(i), voice->products[i]);
|
||||
+ deliver_data(mem, queue_copy, queue_set, voice, static_cast<uint8_t>(i), voice->products[i]);
|
||||
}
|
||||
BIN
defects/vita3k-0001/test/Vita3kNgsDeliverDataTest$Patch.class
Normal file
BIN
defects/vita3k-0001/test/Vita3kNgsDeliverDataTest$Patch.class
Normal file
Binary file not shown.
BIN
defects/vita3k-0001/test/Vita3kNgsDeliverDataTest$Voice.class
Normal file
BIN
defects/vita3k-0001/test/Vita3kNgsDeliverDataTest$Voice.class
Normal file
Binary file not shown.
BIN
defects/vita3k-0001/test/Vita3kNgsDeliverDataTest.class
Normal file
BIN
defects/vita3k-0001/test/Vita3kNgsDeliverDataTest.class
Normal file
Binary file not shown.
177
defects/vita3k-0001/test/Vita3kNgsDeliverDataTest.java
Normal file
177
defects/vita3k-0001/test/Vita3kNgsDeliverDataTest.java
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Unit test for Vita3K CWE-407 defect vita3k-0001.
|
||||
*
|
||||
* vita3k-0001: deliver_data() in vita3k/ngs/src/route.cpp calls
|
||||
* vector_utils::contains(voice_queue, patch->dest) — a linear O(N) scan —
|
||||
* inside a loop over all output patches per voice per audio frame.
|
||||
* With V voices × P patches × V queue size = O(V × P × V) = O(V² × P).
|
||||
*
|
||||
* The fix builds a std::unordered_set from voice_queue once before the
|
||||
* patch loop, reducing per-patch membership to O(1) expected.
|
||||
*
|
||||
* This Java test models the C++ data structure and validates the speedup.
|
||||
*/
|
||||
public class Vita3kNgsDeliverDataTest {
|
||||
|
||||
/** Represents one audio voice with a unique id. */
|
||||
static class Voice {
|
||||
final int id;
|
||||
Voice(int id) { this.id = id; }
|
||||
|
||||
@Override
|
||||
public int hashCode() { return id; }
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
return o instanceof Voice && ((Voice) o).id == this.id;
|
||||
}
|
||||
}
|
||||
|
||||
/** Represents one output patch: (source voice, destination voice). */
|
||||
static class Patch {
|
||||
final Voice dest;
|
||||
Patch(Voice dest) { this.dest = dest; }
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// DEFECTIVE: O(N) linear scan of voice_queue per patch
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/** Models vector_utils::contains — linear scan. */
|
||||
static boolean vectorContains(List<Voice> queue, Voice target) {
|
||||
for (Voice v : queue) {
|
||||
if (v.equals(target)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defective deliver_data: calls vectorContains(queue, patch.dest)
|
||||
* for every patch on every voice. Returns total comparisons made.
|
||||
*/
|
||||
static long defectiveDeliverAll(
|
||||
List<Voice> voiceQueue, Map<Voice, List<Patch>> patchMap) {
|
||||
long comparisons = 0;
|
||||
for (Voice source : voiceQueue) {
|
||||
List<Patch> patches = patchMap.getOrDefault(source, Collections.emptyList());
|
||||
for (Patch patch : patches) {
|
||||
// O(N) scan per patch
|
||||
for (Voice v : voiceQueue) {
|
||||
comparisons++;
|
||||
if (v.equals(patch.dest)) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return comparisons;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// FIXED: O(1) hash-set lookup per patch
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Fixed deliver_data: builds a HashSet once, then O(1) per patch.
|
||||
* Returns total comparisons made (always 1 per patch).
|
||||
*/
|
||||
static long fixedDeliverAll(
|
||||
List<Voice> voiceQueue, Map<Voice, List<Patch>> patchMap) {
|
||||
// Build the set once — mirrors queue_set in the patch
|
||||
Set<Voice> queueSet = new HashSet<>(voiceQueue);
|
||||
long comparisons = 0;
|
||||
for (Voice source : voiceQueue) {
|
||||
List<Patch> patches = patchMap.getOrDefault(source, Collections.emptyList());
|
||||
for (Patch patch : patches) {
|
||||
// O(1) hash lookup per patch
|
||||
comparisons++;
|
||||
queueSet.contains(patch.dest); // lookup, result used implicitly
|
||||
}
|
||||
}
|
||||
return comparisons;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/** Build a voice queue of N voices, each with P patches to random voices. */
|
||||
static Object[] buildScene(int voiceCount, int patchesPerVoice, long seed) {
|
||||
Random rng = new Random(seed);
|
||||
List<Voice> queue = new ArrayList<>();
|
||||
for (int i = 0; i < voiceCount; i++) {
|
||||
queue.add(new Voice(i));
|
||||
}
|
||||
Map<Voice, List<Patch>> patches = new HashMap<>();
|
||||
for (Voice v : queue) {
|
||||
List<Patch> list = new ArrayList<>();
|
||||
for (int p = 0; p < patchesPerVoice; p++) {
|
||||
Voice dest = queue.get(rng.nextInt(voiceCount));
|
||||
list.add(new Patch(dest));
|
||||
}
|
||||
patches.put(v, list);
|
||||
}
|
||||
return new Object[]{queue, patches};
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
int pass = 0;
|
||||
int fail = 0;
|
||||
|
||||
// Test across increasing voice counts
|
||||
int[] voiceCounts = {10, 50, 100, 200};
|
||||
int patchesPerVoice = 4;
|
||||
|
||||
System.out.printf("%-10s %-20s %-20s %-10s%n",
|
||||
"Voices", "Defective(ops)", "Fixed(ops)", "Ratio");
|
||||
|
||||
for (int V : voiceCounts) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Object[] scene = buildScene(V, patchesPerVoice, 42L + V);
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Voice> queue = (List<Voice>) scene[0];
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<Voice, List<Patch>> patchMap = (Map<Voice, List<Patch>>) scene[1];
|
||||
|
||||
long defOps = defectiveDeliverAll(queue, patchMap);
|
||||
long fixOps = fixedDeliverAll(queue, patchMap);
|
||||
|
||||
double ratio = (double) defOps / fixOps;
|
||||
|
||||
System.out.printf("%-10d %-20d %-20d %-10.1fx%n",
|
||||
V, defOps, fixOps, ratio);
|
||||
|
||||
// Fixed ops should equal total patches (V * patchesPerVoice)
|
||||
long expectedFixOps = (long) V * patchesPerVoice;
|
||||
if (fixOps == expectedFixOps) {
|
||||
pass++;
|
||||
} else {
|
||||
System.out.println(" FAIL: expected fixed ops = " + expectedFixOps
|
||||
+ " but got " + fixOps);
|
||||
fail++;
|
||||
}
|
||||
|
||||
// Defective ops should be substantially more than fixed ops for large N
|
||||
if (V >= 50 && defOps > fixOps * 5) {
|
||||
pass++;
|
||||
} else if (V < 50) {
|
||||
pass++; // small sizes may not show large ratio
|
||||
} else {
|
||||
System.out.println(" FAIL: expected defective >> fixed at V=" + V
|
||||
+ ", ratio=" + ratio);
|
||||
fail++;
|
||||
}
|
||||
|
||||
// Correctness: both should process same number of patches
|
||||
// (we verify via defOps / V ≈ patchesPerVoice * average scan)
|
||||
}
|
||||
|
||||
System.out.println();
|
||||
System.out.println("Results: " + pass + " passed, " + fail + " failed");
|
||||
if (fail > 0) {
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
73
defects/yabause-0001/patch/yabause-0001.patch
Normal file
73
defects/yabause-0001/patch/yabause-0001.patch
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# UNDF: UNDF-2026-XXXXXXXXX
|
||||
--- a/yabause/src/netlink.c
|
||||
+++ b/yabause/src/netlink.c
|
||||
@@ -579,10 +579,11 @@ static void NetlinkWriteByte(u32 addr, u8 val)
|
||||
else if (NetlinkArea->connectstatus == NL_CONNECTSTATUS_LOGIN1 &&
|
||||
NetlinkArea->modemstate == NL_MODEMSTATE_DATA &&
|
||||
val == 0x0D)
|
||||
{
|
||||
// Internet login name
|
||||
NetlinkArea->connectstatus = NL_CONNECTSTATUS_LOGIN2;
|
||||
- NETLINK_LOG("login response: %s", NetlinkArea->inbuffer+NetlinkArea->inbufferstart);
|
||||
+ /* Do not log the login name verbatim — credentials in cleartext (CWE-312). */
|
||||
+ NETLINK_LOG("login response: [REDACTED %d bytes]", (int)strlen(NetlinkArea->inbuffer+NetlinkArea->inbufferstart));
|
||||
NetlinkDoATResponse("\r\npassword:");
|
||||
NetlinkUpdateReceivedDataInt();
|
||||
}
|
||||
@@ -588,8 +589,9 @@ static void NetlinkWriteByte(u32 addr, u8 val)
|
||||
else if (NetlinkArea->connectstatus == NL_CONNECTSTATUS_LOGIN2 &&
|
||||
NetlinkArea->modemstate == NL_MODEMSTATE_DATA &&
|
||||
val == 0x0D)
|
||||
{
|
||||
// Internet password
|
||||
NetlinkArea->connectstatus = NL_CONNECTSTATUS_LOGIN3;
|
||||
- NETLINK_LOG("password response: %s", NetlinkArea->inbuffer+NetlinkArea->inbufferstart);
|
||||
+ /* Do not log the password verbatim — credentials in cleartext (CWE-312). */
|
||||
+ NETLINK_LOG("password response: [REDACTED %d bytes]", (int)strlen(NetlinkArea->inbuffer+NetlinkArea->inbufferstart));
|
||||
NetlinkDoATResponse("\r\n$");
|
||||
NetlinkUpdateReceivedDataInt();
|
||||
}
|
||||
|
||||
# Defect: yabause-0001
|
||||
# MOAD: 0004 (CWE-312 — Cleartext Storage of Sensitive Information)
|
||||
# File: yabause/src/netlink.c
|
||||
# Function: NetlinkWriteByte
|
||||
# Lines: 582, 592
|
||||
#
|
||||
# Description:
|
||||
# The Sega Saturn NetLink modem emulation (yabause/src/netlink.c) handles
|
||||
# Saturn online dial-up sessions. During the PPP/shell login handshake the
|
||||
# emulator exchanges a username and password with the remote server on behalf
|
||||
# of the emulated Saturn game.
|
||||
#
|
||||
# When NETLINK_DEBUG is defined at compile time, two NETLINK_LOG calls write
|
||||
# the authentication credentials verbatim into the debug log:
|
||||
#
|
||||
# Line 582: NETLINK_LOG("login response: %s", inbuffer+inbufferstart)
|
||||
# -- logs the Saturn internet login name as a cleartext string
|
||||
#
|
||||
# Line 592: NETLINK_LOG("password response: %s", inbuffer+inbufferstart)
|
||||
# -- logs the Saturn internet password as a cleartext string
|
||||
#
|
||||
# NETLINK_LOG expands to DebugPrintf(MainLog, ...) which writes to MainLog
|
||||
# (a persistent file-backed debug log, see debug.h). Any log file or terminal
|
||||
# session that captures NETLINK_DEBUG output will contain the plaintext
|
||||
# username and password used to authenticate the Saturn session.
|
||||
#
|
||||
# Severity: MEDIUM
|
||||
# - Requires NETLINK_DEBUG compile-time flag (debug/developer builds)
|
||||
# - Exposes Saturn internet credentials (ISP username + password) in cleartext
|
||||
# - Credentials could authenticate a real ISP account if a user tests with
|
||||
# real legacy credentials (e.g. Sega Net, NetLink ISP accounts)
|
||||
# - Log files written to disk may persist and be included in crash reports
|
||||
#
|
||||
# Fix:
|
||||
# Replace the verbatim credential string with a redacted placeholder that
|
||||
# preserves the diagnostic byte-count without exposing credential content.
|
||||
# The fix applies to both the login name log (line 582) and the password
|
||||
# log (line 592).
|
||||
#
|
||||
# References:
|
||||
# - CWE-312: Cleartext Storage of Sensitive Information
|
||||
# - NETLINK_LOG macro: yabause/src/debug.h line 65
|
||||
# - NL_CONNECTSTATUS states: yabause/src/netlink.h lines 45-47
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
defects/yabause-0001/test/YabauseNetlinkCredentialLogTest.class
Normal file
BIN
defects/yabause-0001/test/YabauseNetlinkCredentialLogTest.class
Normal file
Binary file not shown.
217
defects/yabause-0001/test/YabauseNetlinkCredentialLogTest.java
Normal file
217
defects/yabause-0001/test/YabauseNetlinkCredentialLogTest.java
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Unit test for yabause CWE-312 defect yabause-0001.
|
||||
*
|
||||
* yabause-0001: In yabause/src/netlink.c, NetlinkWriteByte() handles the
|
||||
* Saturn NetLink modem dial-up login sequence. When NETLINK_DEBUG is
|
||||
* compiled in, two NETLINK_LOG calls write the Saturn internet credentials
|
||||
* verbatim into the debug log:
|
||||
*
|
||||
* Line 582: NETLINK_LOG("login response: %s", inbuffer+inbufferstart)
|
||||
* Line 592: NETLINK_LOG("password response: %s", inbuffer+inbufferstart)
|
||||
*
|
||||
* This test models the C logging logic and verifies the redaction fix.
|
||||
*/
|
||||
public class YabauseNetlinkCredentialLogTest {
|
||||
|
||||
/** Connection state machine, mirroring NL_CONNECTSTATUS_* enum. */
|
||||
enum ConnectStatus {
|
||||
IDLE, LOGIN1, LOGIN2, LOGIN3, CONNECTED
|
||||
}
|
||||
|
||||
/** Simulates the NETLINK_LOG debug output sink. */
|
||||
static class NetlinkLog {
|
||||
private final List<String> entries = new ArrayList<>();
|
||||
|
||||
void log(String msg) {
|
||||
entries.add(msg);
|
||||
}
|
||||
|
||||
boolean anyEntryContains(String substring) {
|
||||
for (String e : entries) {
|
||||
if (e.contains(substring)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
List<String> getEntries() {
|
||||
return Collections.unmodifiableList(entries);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// DEFECTIVE: logs login name and password verbatim
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
static ConnectStatus defectiveHandleCarriageReturn(
|
||||
NetlinkLog log, ConnectStatus status, String inbuffer) {
|
||||
|
||||
if (status == ConnectStatus.LOGIN1) {
|
||||
// Line 582 — logs login name verbatim
|
||||
log.log(String.format("login response: %s", inbuffer));
|
||||
return ConnectStatus.LOGIN2;
|
||||
}
|
||||
if (status == ConnectStatus.LOGIN2) {
|
||||
// Line 592 — logs password verbatim
|
||||
log.log(String.format("password response: %s", inbuffer));
|
||||
return ConnectStatus.LOGIN3;
|
||||
}
|
||||
if (status == ConnectStatus.LOGIN3) {
|
||||
log.log(String.format("shell response: %s", inbuffer));
|
||||
return ConnectStatus.CONNECTED;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// FIXED: redacts credentials, logs byte count only
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
static ConnectStatus fixedHandleCarriageReturn(
|
||||
NetlinkLog log, ConnectStatus status, String inbuffer) {
|
||||
|
||||
if (status == ConnectStatus.LOGIN1) {
|
||||
// Patched line 582: redacted
|
||||
log.log(String.format("login response: [REDACTED %d bytes]",
|
||||
inbuffer.length()));
|
||||
return ConnectStatus.LOGIN2;
|
||||
}
|
||||
if (status == ConnectStatus.LOGIN2) {
|
||||
// Patched line 592: redacted
|
||||
log.log(String.format("password response: [REDACTED %d bytes]",
|
||||
inbuffer.length()));
|
||||
return ConnectStatus.LOGIN3;
|
||||
}
|
||||
if (status == ConnectStatus.LOGIN3) {
|
||||
log.log(String.format("shell response: %s", inbuffer));
|
||||
return ConnectStatus.CONNECTED;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Test cases
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
static class TestCase {
|
||||
final String loginName;
|
||||
final String password;
|
||||
|
||||
TestCase(String loginName, String password) {
|
||||
this.loginName = loginName;
|
||||
this.password = password;
|
||||
}
|
||||
}
|
||||
|
||||
static final TestCase[] CASES = {
|
||||
new TestCase("saturn_user", "hunter2"),
|
||||
new TestCase("netlink_player", "S3cr3tP@ss"),
|
||||
new TestCase("segaNet001", "CorrectHorseBattery"),
|
||||
};
|
||||
|
||||
public static void main(String[] args) {
|
||||
int pass = 0;
|
||||
int fail = 0;
|
||||
|
||||
for (TestCase tc : CASES) {
|
||||
// --- DEFECTIVE path ---
|
||||
NetlinkLog defLog = new NetlinkLog();
|
||||
ConnectStatus s = ConnectStatus.LOGIN1;
|
||||
|
||||
s = defectiveHandleCarriageReturn(defLog, s, tc.loginName);
|
||||
s = defectiveHandleCarriageReturn(defLog, s, tc.password);
|
||||
defectiveHandleCarriageReturn(defLog, s, "$ ");
|
||||
|
||||
boolean defExpLogin = defLog.anyEntryContains(tc.loginName);
|
||||
boolean defExpPass = defLog.anyEntryContains(tc.password);
|
||||
|
||||
if (defExpLogin) {
|
||||
System.out.println("PASS (defect confirmed): login name '"
|
||||
+ tc.loginName + "' appears in defective log");
|
||||
pass++;
|
||||
} else {
|
||||
System.out.println("FAIL (defect NOT confirmed): login name '"
|
||||
+ tc.loginName + "' missing from defective log");
|
||||
fail++;
|
||||
}
|
||||
|
||||
if (defExpPass) {
|
||||
System.out.println("PASS (defect confirmed): password '"
|
||||
+ tc.password + "' appears in defective log");
|
||||
pass++;
|
||||
} else {
|
||||
System.out.println("FAIL (defect NOT confirmed): password '"
|
||||
+ tc.password + "' missing from defective log");
|
||||
fail++;
|
||||
}
|
||||
|
||||
// --- FIXED path ---
|
||||
NetlinkLog fixLog = new NetlinkLog();
|
||||
s = ConnectStatus.LOGIN1;
|
||||
|
||||
s = fixedHandleCarriageReturn(fixLog, s, tc.loginName);
|
||||
s = fixedHandleCarriageReturn(fixLog, s, tc.password);
|
||||
fixedHandleCarriageReturn(fixLog, s, "$ ");
|
||||
|
||||
boolean fixExpLogin = fixLog.anyEntryContains(tc.loginName);
|
||||
boolean fixExpPass = fixLog.anyEntryContains(tc.password);
|
||||
|
||||
if (!fixExpLogin) {
|
||||
System.out.println("PASS (fix confirmed): login name '"
|
||||
+ tc.loginName + "' NOT in fixed log");
|
||||
pass++;
|
||||
} else {
|
||||
System.out.println("FAIL (fix broken): login name '"
|
||||
+ tc.loginName + "' still appears in fixed log");
|
||||
fail++;
|
||||
}
|
||||
|
||||
if (!fixExpPass) {
|
||||
System.out.println("PASS (fix confirmed): password '"
|
||||
+ tc.password + "' NOT in fixed log");
|
||||
pass++;
|
||||
} else {
|
||||
System.out.println("FAIL (fix broken): password '"
|
||||
+ tc.password + "' still appears in fixed log");
|
||||
fail++;
|
||||
}
|
||||
|
||||
// Redaction markers must be present
|
||||
boolean hasLoginRedact = fixLog.anyEntryContains("login response: [REDACTED");
|
||||
boolean hasPassRedact = fixLog.anyEntryContains("password response: [REDACTED");
|
||||
|
||||
if (hasLoginRedact) {
|
||||
System.out.println("PASS (login redaction marker present)");
|
||||
pass++;
|
||||
} else {
|
||||
System.out.println("FAIL (login redaction marker missing)");
|
||||
fail++;
|
||||
}
|
||||
|
||||
if (hasPassRedact) {
|
||||
System.out.println("PASS (password redaction marker present)");
|
||||
pass++;
|
||||
} else {
|
||||
System.out.println("FAIL (password redaction marker missing)");
|
||||
fail++;
|
||||
}
|
||||
|
||||
// Shell response (non-credential) must still log verbatim
|
||||
boolean shellVisible = fixLog.anyEntryContains("shell response: $ ");
|
||||
if (shellVisible) {
|
||||
System.out.println("PASS (non-credential shell response visible)");
|
||||
pass++;
|
||||
} else {
|
||||
System.out.println("FAIL (non-credential shell response missing)");
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println();
|
||||
System.out.println("Results: " + pass + " passed, " + fail + " failed");
|
||||
if (fail > 0) {
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
66
defects/zesarux-0001/patch/zesarux-0001.patch
Normal file
66
defects/zesarux-0001/patch/zesarux-0001.patch
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# UNDF: UNDF-2026-XXXXXXXXX
|
||||
--- a/src/zrcp/remote.c
|
||||
+++ b/src/zrcp/remote.c
|
||||
@@ -3881,7 +3881,9 @@ static void process_remote_command(int misocket, char *comando)
|
||||
int longitud_comando=strlen(comando);
|
||||
|
||||
if (longitud_comando<DEBUG_MAX_MESSAGE_LENGTH) {
|
||||
- debug_printf (VERBOSE_DEBUG,"Remote command: length: %d [%s]",longitud_comando,comando);
|
||||
+ /* Do not log the full command string verbatim — ZENG commands carry
|
||||
+ creator_pass / user_pass as inline parameters (CWE-312). */
|
||||
+ debug_printf (VERBOSE_DEBUG,"Remote command: length: %d",longitud_comando);
|
||||
}
|
||||
|
||||
else {
|
||||
@@ -3960,11 +3962,11 @@ static void process_remote_command(int misocket, char *comando)
|
||||
debug_printf (VERBOSE_DEBUG,"Remote command without parameters: length: %d [%s]",strlen(comando_sin_parametros),comando_sin_parametros);
|
||||
|
||||
if (strlen(parametros)<DEBUG_MAX_MESSAGE_LENGTH) {
|
||||
- debug_printf (VERBOSE_DEBUG,"Remote command parameters: length: %d [%s]",strlen(parametros),parametros);
|
||||
+ /* Suppress parameter values — first token is creator_pass or user_pass
|
||||
+ for most ZENG multiplayer commands (CWE-312). */
|
||||
+ debug_printf (VERBOSE_DEBUG,"Remote command parameters: length: %d [REDACTED]",strlen(parametros));
|
||||
}
|
||||
else {
|
||||
- debug_printf (VERBOSE_DEBUG,"Remote command parameters: length: %d",strlen(parametros));
|
||||
+ debug_printf (VERBOSE_DEBUG,"Remote command parameters: length: %d [REDACTED]",strlen(parametros));
|
||||
}
|
||||
|
||||
# Defect: zesarux-0001
|
||||
# MOAD: 0004 (CWE-312 — Cleartext Storage of Sensitive Information)
|
||||
# File: src/zrcp/remote.c
|
||||
# Function: process_remote_command (inferred from context)
|
||||
# Lines: 3884, 3963, 3966
|
||||
#
|
||||
# Description:
|
||||
# The ZRCP remote command protocol processes ZENG (ZEsarUX aNd Games) online
|
||||
# multiplayer commands. Dozens of ZENG commands carry authentication tokens as
|
||||
# inline parameters: creator_pass (room owner password) and user_pass (session
|
||||
# token). Examples include authorize-join, destroy-room, get-keys, kick, leave,
|
||||
# put-snapshot, rename-room, send-keys, send-message, set-max-players, and more.
|
||||
#
|
||||
# When VERBOSE_DEBUG logging is active, the ZRCP dispatcher logs:
|
||||
# 1. The full raw command string at line 3884:
|
||||
# "Remote command: length: %d [%s]" -- exposes creator_pass/user_pass
|
||||
# 2. The parameter string at line 3963:
|
||||
# "Remote command parameters: length: %d [%s]" -- exposes the token
|
||||
# directly as the first token in parametros
|
||||
#
|
||||
# Any log file, terminal capture, or remote log aggregator that captures
|
||||
# VERBOSE_DEBUG output will contain the plaintext session credentials.
|
||||
#
|
||||
# Severity: MEDIUM
|
||||
# - Requires VERBOSE_DEBUG log level to be active (debug builds / --verbose-debug)
|
||||
# - Exposes multiplayer session tokens (creator_pass, user_pass) that grant
|
||||
# control over online game rooms hosted via ZENG server mode
|
||||
# - Authentication tokens are typically short random strings, not cryptographic
|
||||
# secrets, but exposure allows room hijacking
|
||||
#
|
||||
# Fix:
|
||||
# Remove the command body from the full-command log message (length only).
|
||||
# Replace the parameter value log with a [REDACTED] placeholder that preserves
|
||||
# the length diagnostic without exposing credential content.
|
||||
#
|
||||
# References:
|
||||
# - CWE-312: Cleartext Storage of Sensitive Information
|
||||
# - ZENG command reference: src/zrcp/remote.c lines 920-962
|
||||
Binary file not shown.
BIN
defects/zesarux-0001/test/ZesaruxZrcpCredentialLogTest.class
Normal file
BIN
defects/zesarux-0001/test/ZesaruxZrcpCredentialLogTest.class
Normal file
Binary file not shown.
153
defects/zesarux-0001/test/ZesaruxZrcpCredentialLogTest.java
Normal file
153
defects/zesarux-0001/test/ZesaruxZrcpCredentialLogTest.java
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
import java.util.*;
|
||||
import java.util.regex.*;
|
||||
|
||||
/**
|
||||
* Unit test for ZEsarUX CWE-312 defect zesarux-0001.
|
||||
*
|
||||
* zesarux-0001: The ZRCP remote command dispatcher logs the full command
|
||||
* string and parameter string verbatim at VERBOSE_DEBUG level. ZENG
|
||||
* multiplayer commands carry creator_pass and user_pass as inline parameters
|
||||
* (e.g. "authorize-join SECRET123 0 rw"). Any debug log or terminal capture
|
||||
* will contain the plaintext session credential.
|
||||
*
|
||||
* Models the C logging behaviour and verifies the fix (redaction).
|
||||
*/
|
||||
public class ZesaruxZrcpCredentialLogTest {
|
||||
|
||||
/** Simulates a debug log sink. */
|
||||
static class DebugLog {
|
||||
private final List<String> entries = new ArrayList<>();
|
||||
|
||||
void log(String msg) {
|
||||
entries.add(msg);
|
||||
}
|
||||
|
||||
List<String> getEntries() {
|
||||
return Collections.unmodifiableList(entries);
|
||||
}
|
||||
|
||||
boolean anyEntryContains(String substring) {
|
||||
for (String e : entries) {
|
||||
if (e.contains(substring)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// DEFECTIVE: logs full command and parameter strings verbatim
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
static void defectiveProcessCommand(DebugLog log, String command) {
|
||||
int len = command.length();
|
||||
// Line 3884: logs full command including inline password
|
||||
log.log(String.format("Remote command: length: %d [%s]", len, command));
|
||||
|
||||
// Split into verb + parameters (simple split on first space)
|
||||
int spaceIdx = command.indexOf(' ');
|
||||
String verb = (spaceIdx >= 0) ? command.substring(0, spaceIdx) : command;
|
||||
String params = (spaceIdx >= 0) ? command.substring(spaceIdx + 1) : "";
|
||||
|
||||
log.log(String.format("Remote command without parameters: length: %d [%s]",
|
||||
verb.length(), verb));
|
||||
|
||||
// Line 3963: logs parameters verbatim — exposes password as first token
|
||||
log.log(String.format("Remote command parameters: length: %d [%s]",
|
||||
params.length(), params));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// FIXED: logs length only, redacts parameter values
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
static void fixedProcessCommand(DebugLog log, String command) {
|
||||
int len = command.length();
|
||||
// Patched line 3884: length only, no body
|
||||
log.log(String.format("Remote command: length: %d", len));
|
||||
|
||||
int spaceIdx = command.indexOf(' ');
|
||||
String verb = (spaceIdx >= 0) ? command.substring(0, spaceIdx) : command;
|
||||
String params = (spaceIdx >= 0) ? command.substring(spaceIdx + 1) : "";
|
||||
|
||||
log.log(String.format("Remote command without parameters: length: %d [%s]",
|
||||
verb.length(), verb));
|
||||
|
||||
// Patched line 3963: parameter length + REDACTED placeholder
|
||||
log.log(String.format("Remote command parameters: length: %d [REDACTED]",
|
||||
params.length()));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// ZENG commands that carry a password as first or later parameter
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
static final String[][] CREDENTIAL_COMMANDS = {
|
||||
{"authorize-join", "CREATORPASS_ALPHA 0 rw"},
|
||||
{"destroy-room", "CREATORPASS_BETA 1"},
|
||||
{"get-keys", "USERPASS_GAMMA 0"},
|
||||
{"kick", "CREATORPASS_DELTA 0 uuid-1234"},
|
||||
{"leave", "0 USERPASS_EPSILON uuid-5678"},
|
||||
{"rename-room", "CREATORPASS_ZETA 0 MyRoom"},
|
||||
{"put-snapshot", "CREATORPASS_ETA 0 DEADBEEF"},
|
||||
{"send-keys", "USERPASS_THETA 0 uuid-9999 65 1 0"},
|
||||
};
|
||||
|
||||
public static void main(String[] args) {
|
||||
int pass = 0;
|
||||
int fail = 0;
|
||||
|
||||
for (String[] entry : CREDENTIAL_COMMANDS) {
|
||||
String verb = entry[0];
|
||||
String params = entry[1];
|
||||
// Extract the password token (first token of params)
|
||||
String password = params.split(" ")[0];
|
||||
String command = verb + " " + params;
|
||||
|
||||
// -- DEFECTIVE: password must appear in log --
|
||||
DebugLog defLog = new DebugLog();
|
||||
defectiveProcessCommand(defLog, command);
|
||||
|
||||
boolean defectiveExposes = defLog.anyEntryContains(password);
|
||||
if (defectiveExposes) {
|
||||
System.out.println("PASS (defect confirmed): " + verb
|
||||
+ " exposes '" + password + "' in log");
|
||||
pass++;
|
||||
} else {
|
||||
System.out.println("FAIL (defect NOT confirmed): " + verb
|
||||
+ " did not expose '" + password + "' — test is wrong");
|
||||
fail++;
|
||||
}
|
||||
|
||||
// -- FIXED: password must NOT appear in log --
|
||||
DebugLog fixLog = new DebugLog();
|
||||
fixedProcessCommand(fixLog, command);
|
||||
|
||||
boolean fixedExposes = fixLog.anyEntryContains(password);
|
||||
if (!fixedExposes) {
|
||||
System.out.println("PASS (fix confirmed): " + verb
|
||||
+ " does NOT expose '" + password + "' after fix");
|
||||
pass++;
|
||||
} else {
|
||||
System.out.println("FAIL (fix broken): " + verb
|
||||
+ " still exposes '" + password + "' after fix");
|
||||
fail++;
|
||||
}
|
||||
|
||||
// -- FIXED: [REDACTED] placeholder must appear --
|
||||
boolean hasRedacted = fixLog.anyEntryContains("[REDACTED]");
|
||||
if (hasRedacted) {
|
||||
System.out.println("PASS (redaction marker present): " + verb);
|
||||
pass++;
|
||||
} else {
|
||||
System.out.println("FAIL (no redaction marker): " + verb);
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println();
|
||||
System.out.println("Results: " + pass + " passed, " + fail + " failed");
|
||||
if (fail > 0) {
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue