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