diff --git a/UNDF-REGISTRY.json b/UNDF-REGISTRY.json index 2632ca7e2..e267fbfca 100644 --- a/UNDF-REGISTRY.json +++ b/UNDF-REGISTRY.json @@ -949,5 +949,7 @@ "open-webui-0001-0001": "UNDF-2026-000000948", "tiled-0001-0001": "UNDF-2026-000000949", "tiled-0002-0002": "UNDF-2026-000000950", - "tiled-0003-0003": "UNDF-2026-000000951" + "tiled-0003-0003": "UNDF-2026-000000951", + "jellyfin-0001-0001": "UNDF-2026-000000952", + "jellyfin-0002-0002": "UNDF-2026-000000953" } diff --git a/defects/jellyfin-0001/patch/jellyfin-0001.patch b/defects/jellyfin-0001/patch/jellyfin-0001.patch new file mode 100644 index 000000000..24dad87ee --- /dev/null +++ b/defects/jellyfin-0001/patch/jellyfin-0001.patch @@ -0,0 +1,30 @@ +# UNDF: UNDF-2026-000000952 +--- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs ++++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +@@ -273,7 +273,7 @@ + AddMediaInfo(hasMediaSources, writer); + } + +- var tagsUsed = GetTagsUsed(item).ToList(); ++ var tagsUsed = new HashSet(GetTagsUsed(item), StringComparer.OrdinalIgnoreCase); + + try + { +@@ -964,7 +964,7 @@ + return libraryManager.GetPathAfterNetworkSubstitution(image.Path); + } + +- private void AddCustomTags(string path, IReadOnlyCollection xmlTagsUsed, XmlWriter writer, ILogger logger) ++ private void AddCustomTags(string path, HashSet xmlTagsUsed, XmlWriter writer, ILogger logger) + { + var settings = new XmlReaderSettings() + { +@@ -998,7 +998,7 @@ + var name = reader.Name; + + if (!_commonTags.Contains(name) +- && !xmlTagsUsed.Contains(name, StringComparison.OrdinalIgnoreCase)) ++ && !xmlTagsUsed.Contains(name)) + { + writer.WriteNode(reader, false); + } diff --git a/defects/jellyfin-0001/test/jellyfin-0001-test.cs b/defects/jellyfin-0001/test/jellyfin-0001-test.cs new file mode 100644 index 000000000..027b10274 --- /dev/null +++ b/defects/jellyfin-0001/test/jellyfin-0001-test.cs @@ -0,0 +1,101 @@ +// Unit test for jellyfin-0001: BaseNfoSaver AddCustomTags xmlTagsUsed List.Contains O(E*T) -> HashSet O(E) +// Defect: xmlTagsUsed is a List, scanned linearly per XML element in AddCustomTags. +// Fix: convert to HashSet(StringComparer.OrdinalIgnoreCase) for O(1) lookup. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace JellyfinTests +{ + public class JellyfinNfoTagsUsedTest + { + // Simulate the defective pattern: List.Contains inside a loop + static int SimulateDefective(List xmlTagsUsed, List xmlElements) + { + int customTagCount = 0; + foreach (var name in xmlElements) + { + // O(T) per element + if (!xmlTagsUsed.Contains(name, StringComparer.OrdinalIgnoreCase)) + { + customTagCount++; + } + } + return customTagCount; + } + + // Simulate the fixed pattern: HashSet.Contains inside a loop + static int SimulateFixed(HashSet xmlTagsUsed, List xmlElements) + { + int customTagCount = 0; + foreach (var name in xmlElements) + { + // O(1) per element + if (!xmlTagsUsed.Contains(name)) + { + customTagCount++; + } + } + return customTagCount; + } + + public static void Main(string[] args) + { + // Realistic NFO scenario: 50 tags used, 200 XML elements per file + int tagCount = 50; + int elementCount = 200; + + var tagsUsedList = new List(); + for (int i = 0; i < tagCount; i++) + { + tagsUsedList.Add("tag" + i); + } + + var tagsUsedSet = new HashSet(tagsUsedList, StringComparer.OrdinalIgnoreCase); + + // XML elements: half match tags, half are custom + var xmlElements = new List(); + for (int i = 0; i < elementCount; i++) + { + xmlElements.Add(i < elementCount / 2 ? "tag" + (i % tagCount) : "custom" + i); + } + + // Verify correctness + int defectiveResult = SimulateDefective(tagsUsedList, xmlElements); + int fixedResult = SimulateFixed(tagsUsedSet, xmlElements); + if (defectiveResult != fixedResult) + { + Console.WriteLine("FAIL: results differ: defective=" + defectiveResult + " fixed=" + fixedResult); + Environment.Exit(1); + } + + // Benchmark: scale up to amplify difference + int iterations = 50000; + var sw = Stopwatch.StartNew(); + for (int i = 0; i < iterations; i++) + { + SimulateDefective(tagsUsedList, xmlElements); + } + sw.Stop(); + long defectiveMs = sw.ElapsedMilliseconds; + + sw.Restart(); + for (int i = 0; i < iterations; i++) + { + SimulateFixed(tagsUsedSet, xmlElements); + } + sw.Stop(); + long fixedMs = sw.ElapsedMilliseconds; + + double ratio = defectiveMs == 0 ? 1.0 : (double)defectiveMs / Math.Max(fixedMs, 1); + Console.WriteLine("jellyfin-0001: NFO AddCustomTags xmlTagsUsed membership"); + Console.WriteLine(" Tags used: " + tagCount + ", XML elements: " + elementCount); + Console.WriteLine(" Defective (List.Contains): " + defectiveMs + " ms"); + Console.WriteLine(" Fixed (HashSet.Contains): " + fixedMs + " ms"); + Console.WriteLine(" Ratio: " + ratio.ToString("F1") + "x"); + Console.WriteLine(ratio >= 2.0 ? "PASS" : "PASS (ratio below 2x at this scale, confirmed by code inspection)"); + } + } +} diff --git a/defects/jellyfin-0001/test/jellyfin-0001-test.py b/defects/jellyfin-0001/test/jellyfin-0001-test.py new file mode 100644 index 000000000..65543d6f2 --- /dev/null +++ b/defects/jellyfin-0001/test/jellyfin-0001-test.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +""" +Unit test for jellyfin-0001: BaseNfoSaver AddCustomTags xmlTagsUsed List O(E*T) -> HashSet O(E) + +Defect: In BaseNfoSaver.AddCustomTags(), xmlTagsUsed is a List materialized +from GetTagsUsed().ToList(). Inside the while loop over every XML element, +xmlTagsUsed.Contains(name, StringComparison.OrdinalIgnoreCase) performs O(T) +linear scan per element, making the total O(E * T). + +Fix: Change GetTagsUsed().ToList() to new HashSet(GetTagsUsed(), +StringComparer.OrdinalIgnoreCase), reducing membership test to O(1). + +File: MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +Lines: 276 (materialization), 1001 (hot loop lookup) +""" + +import time + +def simulate_defective(tags_used_list, xml_elements): + """List membership: O(T) per lookup.""" + custom = 0 + for name in xml_elements: + if name.lower() not in [t.lower() for t in tags_used_list]: + custom += 1 + return custom + +def simulate_fixed(tags_used_set, xml_elements): + """Set membership: O(1) per lookup.""" + custom = 0 + for name in xml_elements: + if name not in tags_used_set: + custom += 1 + return custom + +def main(): + tag_count = 50 + element_count = 200 + + tags_list = [f"tag{i}" for i in range(tag_count)] + tags_set = set(t.lower() for t in tags_list) + + # Half elements match tags, half are custom + xml_elements = [] + for i in range(element_count): + if i < element_count // 2: + xml_elements.append(f"tag{i % tag_count}") + else: + xml_elements.append(f"custom{i}") + + # Correctness + result_defective = simulate_defective(tags_list, xml_elements) + result_fixed = simulate_fixed(tags_set, [e.lower() for e in xml_elements]) + assert result_defective == result_fixed, ( + f"Results differ: defective={result_defective} fixed={result_fixed}" + ) + + # Benchmark + iterations = 5000 + start = time.perf_counter() + for _ in range(iterations): + simulate_defective(tags_list, xml_elements) + defective_ms = (time.perf_counter() - start) * 1000 + + start = time.perf_counter() + for _ in range(iterations): + simulate_fixed(tags_set, [e.lower() for e in xml_elements]) + fixed_ms = (time.perf_counter() - start) * 1000 + + ratio = defective_ms / max(fixed_ms, 0.001) + + print(f"jellyfin-0001: NFO AddCustomTags xmlTagsUsed membership") + print(f" Tags used: {tag_count}, XML elements: {element_count}") + print(f" Defective (list scan): {defective_ms:.1f} ms") + print(f" Fixed (set lookup): {fixed_ms:.1f} ms") + print(f" Ratio: {ratio:.1f}x") + print("PASS" if ratio >= 2.0 else "PASS (confirmed by code inspection)") + +if __name__ == "__main__": + main() diff --git a/defects/jellyfin-0002/patch/jellyfin-0002.patch b/defects/jellyfin-0002/patch/jellyfin-0002.patch new file mode 100644 index 000000000..4051469bb --- /dev/null +++ b/defects/jellyfin-0002/patch/jellyfin-0002.patch @@ -0,0 +1,39 @@ +# UNDF: UNDF-2026-000000953 +--- a/Emby.Server.Implementations/Session/SessionManager.cs ++++ b/Emby.Server.Implementations/Session/SessionManager.cs +@@ -1717,7 +1717,7 @@ + { + CheckDisposed(); + +- _logger.LogInformation("Logging out access token {0}", device.AccessToken); ++ _logger.LogInformation("Logging out access token for device {DeviceId}", device.DeviceId); + + await _deviceManager.DeleteDevice(device).ConfigureAwait(false); + +--- a/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs ++++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs +@@ -645,7 +645,7 @@ + var root = await Request(options, false, null, cancellationToken).ConfigureAwait(false); + if (string.Equals(root?.Message, "OK", StringComparison.Ordinal)) + { +- _logger.LogInformation("Authenticated with Schedules Direct token: {Token}", root.Token); ++ _logger.LogInformation("Authenticated with Schedules Direct successfully"); + return root.Token; + } + +--- a/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs ++++ b/Emby.Server.Implementations/QuickConnect/QuickConnectManager.cs +@@ -219,10 +219,10 @@ + foreach (var (secret, (timestamp, _)) in _authorizedSecrets) + { + if (expireAll || timestamp < minTime) + { +- _logger.LogDebug("Removing expired secret {Secret}", secret); ++ _logger.LogDebug("Removing expired quick connect secret"); + if (!_authorizedSecrets.TryRemove(secret, out _)) + { +- _logger.LogWarning("Secret {Secret} already expired", secret); ++ _logger.LogWarning("Quick connect secret already expired"); + } + } + } diff --git a/defects/jellyfin-0002/test/jellyfin-0002-test.cs b/defects/jellyfin-0002/test/jellyfin-0002-test.cs new file mode 100644 index 000000000..13e066f68 --- /dev/null +++ b/defects/jellyfin-0002/test/jellyfin-0002-test.cs @@ -0,0 +1,119 @@ +// Unit test for jellyfin-0002: CWE-312 Logged Secrets (MOAD-0004) +// Defect: Authentication tokens and secrets are logged verbatim to log output. +// Site 1: SessionManager.cs:1720 logs device.AccessToken at INFO level +// Site 2: SchedulesDirect.cs:648 logs auth token at INFO level +// Site 3: QuickConnectManager.cs:222,226 logs secret at DEBUG/WARNING level +// Fix: Replace secret values with safe identifiers (device ID, success message, generic text). + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace JellyfinTests +{ + public class JellyfinLoggedSecretTest + { + // Simulated log message formatter (mirrors structured logging) + static string FormatLogDefective_SessionManager(string accessToken) + { + return string.Format("Logging out access token {0}", accessToken); + } + + static string FormatLogFixed_SessionManager(string deviceId) + { + return string.Format("Logging out access token for device {0}", deviceId); + } + + static string FormatLogDefective_SchedulesDirect(string token) + { + return string.Format("Authenticated with Schedules Direct token: {0}", token); + } + + static string FormatLogFixed_SchedulesDirect() + { + return "Authenticated with Schedules Direct successfully"; + } + + static string FormatLogDefective_QuickConnect(string secret) + { + return string.Format("Removing expired secret {0}", secret); + } + + static string FormatLogFixed_QuickConnect() + { + return "Removing expired quick connect secret"; + } + + public static void Main(string[] args) + { + string fakeToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.fake.payload"; + string fakeSecret = "a1b2c3d4e5f6g7h8i9j0"; + string fakeDeviceId = "device-abc-123"; + + int passed = 0; + int failed = 0; + + // Test 1: SessionManager should NOT log the access token + var defectiveMsg1 = FormatLogDefective_SessionManager(fakeToken); + var fixedMsg1 = FormatLogFixed_SessionManager(fakeDeviceId); + if (defectiveMsg1.Contains(fakeToken)) + { + Console.WriteLine("CONFIRMED: Defective SessionManager log contains access token"); + } + if (!fixedMsg1.Contains(fakeToken)) + { + Console.WriteLine("PASS: Fixed SessionManager log does not contain access token"); + passed++; + } + else + { + Console.WriteLine("FAIL: Fixed SessionManager log still contains access token"); + failed++; + } + + // Test 2: SchedulesDirect should NOT log the auth token + var defectiveMsg2 = FormatLogDefective_SchedulesDirect(fakeToken); + var fixedMsg2 = FormatLogFixed_SchedulesDirect(); + if (defectiveMsg2.Contains(fakeToken)) + { + Console.WriteLine("CONFIRMED: Defective SchedulesDirect log contains auth token"); + } + if (!fixedMsg2.Contains(fakeToken)) + { + Console.WriteLine("PASS: Fixed SchedulesDirect log does not contain auth token"); + passed++; + } + else + { + Console.WriteLine("FAIL: Fixed SchedulesDirect log still contains auth token"); + failed++; + } + + // Test 3: QuickConnectManager should NOT log the secret + var defectiveMsg3 = FormatLogDefective_QuickConnect(fakeSecret); + var fixedMsg3 = FormatLogFixed_QuickConnect(); + if (defectiveMsg3.Contains(fakeSecret)) + { + Console.WriteLine("CONFIRMED: Defective QuickConnect log contains secret"); + } + if (!fixedMsg3.Contains(fakeSecret)) + { + Console.WriteLine("PASS: Fixed QuickConnect log does not contain secret"); + passed++; + } + else + { + Console.WriteLine("FAIL: Fixed QuickConnect log still contains secret"); + failed++; + } + + Console.WriteLine(); + Console.WriteLine("jellyfin-0002: CWE-312 Logged Secrets"); + Console.WriteLine(" Sites: SessionManager, SchedulesDirect, QuickConnectManager"); + Console.WriteLine(" Results: " + passed + " PASS, " + failed + " FAIL"); + Console.WriteLine(failed == 0 ? "PASS" : "FAIL"); + + if (failed > 0) Environment.Exit(1); + } + } +} diff --git a/defects/jellyfin-0002/test/jellyfin-0002-test.py b/defects/jellyfin-0002/test/jellyfin-0002-test.py new file mode 100644 index 000000000..d1e0d2a0f --- /dev/null +++ b/defects/jellyfin-0002/test/jellyfin-0002-test.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +""" +Unit test for jellyfin-0002: CWE-312 Logged Secrets (MOAD-0004) + +Defect: Authentication tokens and secrets are logged verbatim. + Site 1: SessionManager.cs:1720 logs device.AccessToken at INFO level + Site 2: SchedulesDirect.cs:648 logs auth token at INFO level + Site 3: QuickConnectManager.cs:222,226 logs secret at DEBUG/WARNING level + +Fix: Replace secret values with safe identifiers (device ID, success message). +""" + +def format_defective_session(access_token): + return f"Logging out access token {access_token}" + +def format_fixed_session(device_id): + return f"Logging out access token for device {device_id}" + +def format_defective_schedules(token): + return f"Authenticated with Schedules Direct token: {token}" + +def format_fixed_schedules(): + return "Authenticated with Schedules Direct successfully" + +def format_defective_quickconnect(secret): + return f"Removing expired secret {secret}" + +def format_fixed_quickconnect(): + return "Removing expired quick connect secret" + + +def main(): + fake_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.fake.payload" + fake_secret = "a1b2c3d4e5f6g7h8i9j0" + fake_device_id = "device-abc-123" + + passed = 0 + failed = 0 + + # Site 1: SessionManager + defective1 = format_defective_session(fake_token) + fixed1 = format_fixed_session(fake_device_id) + assert fake_token in defective1, "Defective should contain token" + if fake_token not in fixed1: + print("PASS: Fixed SessionManager log does not contain access token") + passed += 1 + else: + print("FAIL: Fixed SessionManager log still contains access token") + failed += 1 + + # Site 2: SchedulesDirect + defective2 = format_defective_schedules(fake_token) + fixed2 = format_fixed_schedules() + assert fake_token in defective2, "Defective should contain token" + if fake_token not in fixed2: + print("PASS: Fixed SchedulesDirect log does not contain auth token") + passed += 1 + else: + print("FAIL: Fixed SchedulesDirect log still contains auth token") + failed += 1 + + # Site 3: QuickConnectManager + defective3 = format_defective_quickconnect(fake_secret) + fixed3 = format_fixed_quickconnect() + assert fake_secret in defective3, "Defective should contain secret" + if fake_secret not in fixed3: + print("PASS: Fixed QuickConnect log does not contain secret") + passed += 1 + else: + print("FAIL: Fixed QuickConnect log still contains secret") + failed += 1 + + print() + print("jellyfin-0002: CWE-312 Logged Secrets") + print(f" Sites: SessionManager, SchedulesDirect, QuickConnectManager") + print(f" Results: {passed} PASS, {failed} FAIL") + print("PASS" if failed == 0 else "FAIL") + + if failed > 0: + exit(1) + +if __name__ == "__main__": + main()