diff --git a/UNDF-REGISTRY.json b/UNDF-REGISTRY.json index 71aa94f38..fdb8c0ef4 100644 --- a/UNDF-REGISTRY.json +++ b/UNDF-REGISTRY.json @@ -925,5 +925,12 @@ "electrum-0002-0002": "UNDF-2026-000000924", "cake_wallet-0002-0002": "UNDF-2026-000000925", "cake_wallet-0003-0003": "UNDF-2026-000000926", - "cake_wallet-0004-0004": "UNDF-2026-000000927" + "cake_wallet-0004-0004": "UNDF-2026-000000927", + "btcpayserver-0001-0001": "UNDF-2026-000000928", + "btcpayserver-0002-0002": "UNDF-2026-000000929", + "btcpayserver-0003-0003": "UNDF-2026-000000930", + "sparrow-0001-0001": "UNDF-2026-000000931", + "sparrow-0002-0002": "UNDF-2026-000000932", + "trezor-0001-0001": "UNDF-2026-000000933", + "trezor-0002-0002": "UNDF-2026-000000934" } diff --git a/defects/btcpayserver-0001/patch/btcpayserver-0001.patch b/defects/btcpayserver-0001/patch/btcpayserver-0001.patch new file mode 100644 index 000000000..facffede7 --- /dev/null +++ b/defects/btcpayserver-0001/patch/btcpayserver-0001.patch @@ -0,0 +1,24 @@ +# UNDF: UNDF-2026-000000928 +--- a/BTCPayServer/Data/WalletTransactionInfo.cs ++++ b/BTCPayServer/Data/WalletTransactionInfo.cs +@@ -99,10 +99,14 @@ + result.LabelColors = new Dictionary(LabelColors); + result.Attachments = new List(Attachments); ++ ++ // CWE-407: Attachments.Any() inside Where() is O(A*B) where A = value.Attachments ++ // and B = this.Attachments. Build a HashSet of existing (Id, Type) pairs for O(1) lookup. ++ var existingAttachments = new HashSet<(string Id, string Type)>( ++ Attachments.Select(a => (a.Id, a.Type))); ++ + foreach (var valueLabelColor in value.LabelColors) + { + result.LabelColors.TryAdd(valueLabelColor.Key, valueLabelColor.Value); + } + +- foreach (var valueAttachment in value.Attachments.Where(valueAttachment => !Attachments.Any(attachment => +- attachment.Id == valueAttachment.Id && attachment.Type == valueAttachment.Type))) ++ foreach (var valueAttachment in value.Attachments.Where(valueAttachment => ++ !existingAttachments.Contains((valueAttachment.Id, valueAttachment.Type)))) + { + result.Attachments.Add(valueAttachment); + } diff --git a/defects/btcpayserver-0001/test/btcpayserver-0001-test.cs b/defects/btcpayserver-0001/test/btcpayserver-0001-test.cs new file mode 100644 index 000000000..a688ec983 --- /dev/null +++ b/defects/btcpayserver-0001/test/btcpayserver-0001-test.cs @@ -0,0 +1,100 @@ +// Unit test for btcpayserver-0001: WalletTransactionInfo.Merge O(A*B) attachment dedup +// Demonstrates quadratic vs linear behavior when merging wallet transaction attachments. +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace BTCPayServer.Tests.CWE407 +{ + // Minimal Attachment stand-in + public class Attachment + { + public string Id { get; set; } + public string Type { get; set; } + } + + public static class BtcPayServer0001Test + { + // --- ORIGINAL (defective): O(A * B) --- + static List MergeOriginal(List existing, List incoming) + { + var result = new List(existing); + foreach (var va in incoming.Where(va => + !existing.Any(a => a.Id == va.Id && a.Type == va.Type))) + { + result.Add(va); + } + return result; + } + + // --- PATCHED: O(A + B) --- + static List MergePatched(List existing, List incoming) + { + var result = new List(existing); + var existingSet = new HashSet<(string, string)>( + existing.Select(a => (a.Id, a.Type))); + foreach (var va in incoming.Where(va => + !existingSet.Contains((va.Id, va.Type)))) + { + result.Add(va); + } + return result; + } + + static List MakeAttachments(int count, string prefix) + { + var list = new List(count); + for (int i = 0; i < count; i++) + list.Add(new Attachment { Id = $"{prefix}-{i}", Type = "invoice" }); + return list; + } + + public static void Main(string[] args) + { + int N = 2000; + var existing = MakeAttachments(N, "existing"); + var incoming = MakeAttachments(N, "incoming"); // all new, worst case + + // Warmup + MergeOriginal(existing, incoming); + MergePatched(existing, incoming); + + var sw = Stopwatch.StartNew(); + for (int i = 0; i < 5; i++) + MergeOriginal(existing, incoming); + sw.Stop(); + long originalMs = sw.ElapsedMilliseconds; + + sw.Restart(); + for (int i = 0; i < 5; i++) + MergePatched(existing, incoming); + sw.Stop(); + long patchedMs = sw.ElapsedMilliseconds; + + double ratio = (double)originalMs / Math.Max(1, patchedMs); + + Console.WriteLine($"N = {N}"); + Console.WriteLine($"Original (List.Any): {originalMs} ms"); + Console.WriteLine($"Patched (HashSet): {patchedMs} ms"); + Console.WriteLine($"Speedup ratio: {ratio:F1}x"); + + // Correctness: both produce same count + var origResult = MergeOriginal(existing, incoming); + var patchResult = MergePatched(existing, incoming); + bool correctness = origResult.Count == patchResult.Count && + origResult.Count == N + N; + Console.WriteLine($"Correctness: {(correctness ? "PASS" : "FAIL")}"); + + // Dedup correctness: overlapping + var overlap = MakeAttachments(N, "existing"); // same IDs as existing + var origOverlap = MergeOriginal(existing, overlap); + var patchOverlap = MergePatched(existing, overlap); + bool dedupCorrect = origOverlap.Count == N && patchOverlap.Count == N; + Console.WriteLine($"Dedup correctness: {(dedupCorrect ? "PASS" : "FAIL")}"); + + if (!correctness || !dedupCorrect) + Environment.Exit(1); + } + } +} diff --git a/defects/btcpayserver-0002/patch/btcpayserver-0002.patch b/defects/btcpayserver-0002/patch/btcpayserver-0002.patch new file mode 100644 index 000000000..95614d2bd --- /dev/null +++ b/defects/btcpayserver-0002/patch/btcpayserver-0002.patch @@ -0,0 +1,23 @@ +# UNDF: UNDF-2026-000000929 +--- a/BTCPayServer/Services/Apps/AppService.cs ++++ b/BTCPayServer/Services/Apps/AppService.cs +@@ -113,9 +113,12 @@ + + // fill up the gaps ++ // CWE-407: series.All(e => e.Date != date) inside foreach is O(D*S) where ++ // D = numberOfDays and S = series.Count. Build a HashSet for O(1) lookup. ++ var existingDates = new HashSet(series.Select(e => e.Date)); + foreach (var i in Enumerable.Range(0, numberOfDays)) + { + var date = (DateTimeOffset.UtcNow - TimeSpan.FromDays(i)).Date; +- if (series.All(e => e.Date != date)) ++ if (!existingDates.Contains(date)) + { + series.Add(new AppSalesStatsItem + { + Date = date, + Label = date.ToString("MMM dd", CultureInfo.InvariantCulture) + }); ++ existingDates.Add(date); + } + } diff --git a/defects/btcpayserver-0002/test/btcpayserver-0002-test.cs b/defects/btcpayserver-0002/test/btcpayserver-0002-test.cs new file mode 100644 index 000000000..0ad610f7b --- /dev/null +++ b/defects/btcpayserver-0002/test/btcpayserver-0002-test.cs @@ -0,0 +1,114 @@ +// Unit test for btcpayserver-0002: AppService gap-fill O(D*S) series.All inside foreach +// Demonstrates quadratic vs linear behavior when filling date gaps in sales stats. +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace BTCPayServer.Tests.CWE407 +{ + public class SalesItem + { + public DateTime Date { get; set; } + public string Label { get; set; } + public int SalesCount { get; set; } + } + + public static class BtcPayServer0002Test + { + // --- ORIGINAL (defective): O(D * S) --- + static void FillGapsOriginal(List series, int numberOfDays) + { + foreach (var i in Enumerable.Range(0, numberOfDays)) + { + var date = (DateTimeOffset.UtcNow - TimeSpan.FromDays(i)).Date; + if (series.All(e => e.Date != date)) + { + series.Add(new SalesItem { Date = date, Label = date.ToString("MMM dd") }); + } + } + } + + // --- PATCHED: O(D + S) --- + static void FillGapsPatched(List series, int numberOfDays) + { + var existingDates = new HashSet(series.Select(e => e.Date)); + foreach (var i in Enumerable.Range(0, numberOfDays)) + { + var date = (DateTimeOffset.UtcNow - TimeSpan.FromDays(i)).Date; + if (!existingDates.Contains(date)) + { + series.Add(new SalesItem { Date = date, Label = date.ToString("MMM dd") }); + existingDates.Add(date); + } + } + } + + static List MakeSparse(int days, int gapEvery) + { + var list = new List(); + for (int i = 0; i < days; i++) + { + if (i % gapEvery != 0) continue; + var date = (DateTimeOffset.UtcNow - TimeSpan.FromDays(i)).Date; + list.Add(new SalesItem { Date = date, Label = date.ToString("MMM dd"), SalesCount = 1 }); + } + return list; + } + + public static void Main(string[] args) + { + int D = 3000; // exaggerated for benchmark + int gapEvery = 5; + + // Warmup + var warmup1 = MakeSparse(D, gapEvery); + FillGapsOriginal(warmup1, D); + var warmup2 = MakeSparse(D, gapEvery); + FillGapsPatched(warmup2, D); + + var sw = Stopwatch.StartNew(); + for (int run = 0; run < 3; run++) + { + var s = MakeSparse(D, gapEvery); + FillGapsOriginal(s, D); + } + sw.Stop(); + long originalMs = sw.ElapsedMilliseconds; + + sw.Restart(); + for (int run = 0; run < 3; run++) + { + var s = MakeSparse(D, gapEvery); + FillGapsPatched(s, D); + } + sw.Stop(); + long patchedMs = sw.ElapsedMilliseconds; + + double ratio = (double)originalMs / Math.Max(1, patchedMs); + + Console.WriteLine($"D = {D}, gap every {gapEvery} days"); + Console.WriteLine($"Original (List.All): {originalMs} ms"); + Console.WriteLine($"Patched (HashSet): {patchedMs} ms"); + Console.WriteLine($"Speedup ratio: {ratio:F1}x"); + + // Correctness + var orig = MakeSparse(D, gapEvery); + FillGapsOriginal(orig, D); + var patched = MakeSparse(D, gapEvery); + FillGapsPatched(patched, D); + + bool correct = orig.Count == patched.Count && orig.Count == D; + Console.WriteLine($"Count match: {(correct ? "PASS" : "FAIL")} (orig={orig.Count}, patched={patched.Count})"); + + // Verify no duplicate dates + bool noDupsOrig = orig.Select(e => e.Date).Distinct().Count() == orig.Count; + bool noDupsPatched = patched.Select(e => e.Date).Distinct().Count() == patched.Count; + Console.WriteLine($"No dups (original): {(noDupsOrig ? "PASS" : "FAIL")}"); + Console.WriteLine($"No dups (patched): {(noDupsPatched ? "PASS" : "FAIL")}"); + + if (!correct || !noDupsOrig || !noDupsPatched) + Environment.Exit(1); + } + } +} diff --git a/defects/btcpayserver-0003/patch/btcpayserver-0003.patch b/defects/btcpayserver-0003/patch/btcpayserver-0003.patch new file mode 100644 index 000000000..5f35f1d72 --- /dev/null +++ b/defects/btcpayserver-0003/patch/btcpayserver-0003.patch @@ -0,0 +1,26 @@ +# UNDF: UNDF-2026-000000930 +--- a/BTCPayServer.Abstractions/Extensions/StringExtensions.cs ++++ b/BTCPayServer.Abstractions/Extensions/StringExtensions.cs +@@ -1,4 +1,5 @@ + using System; ++using System.Collections.Generic; + using System.IO; + using System.Linq; + +@@ -6,9 +7,16 @@ + + public static class StringExtensions + { ++ // CWE-407: Path.GetInvalidFileNameChars() allocates a new char[] on every call. ++ // Contains() on char[] is O(I) per character. For a filename of length F, this ++ // is O(F * I) with I = ~41 invalid chars, plus F array allocations. ++ // Fix: cache in a static HashSet for O(1) lookup and zero allocation. ++ private static readonly HashSet InvalidFileNameChars = new HashSet(Path.GetInvalidFileNameChars()); ++ + public static bool IsValidFileName(this string fileName) + { +- return !fileName.ToCharArray().Any(c => Path.GetInvalidFileNameChars().Contains(c) ++ return !fileName.ToCharArray().Any(c => InvalidFileNameChars.Contains(c) + || c == Path.AltDirectorySeparatorChar + || c == Path.DirectorySeparatorChar + || c == Path.PathSeparator diff --git a/defects/btcpayserver-0003/test/btcpayserver-0003-test.cs b/defects/btcpayserver-0003/test/btcpayserver-0003-test.cs new file mode 100644 index 000000000..e6bbbe392 --- /dev/null +++ b/defects/btcpayserver-0003/test/btcpayserver-0003-test.cs @@ -0,0 +1,83 @@ +// Unit test for btcpayserver-0003: StringExtensions.IsValidFileName O(F*I) +// Path.GetInvalidFileNameChars() allocates new char[] per call, Contains is O(I). +// Fix: static HashSet for O(1) lookup, zero allocation per call. +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; + +namespace BTCPayServer.Tests.CWE407 +{ + public static class BtcPayServer0003Test + { + // --- ORIGINAL (defective): O(F * I), allocates char[] per char --- + static bool IsValidFileNameOriginal(string fileName) + { + return !fileName.ToCharArray().Any(c => Path.GetInvalidFileNameChars().Contains(c) + || c == Path.AltDirectorySeparatorChar + || c == Path.DirectorySeparatorChar + || c == Path.PathSeparator + || c == '\\'); + } + + // --- PATCHED: O(F), static HashSet --- + private static readonly HashSet InvalidFileNameChars = new HashSet(Path.GetInvalidFileNameChars()); + static bool IsValidFileNamePatched(string fileName) + { + return !fileName.ToCharArray().Any(c => InvalidFileNameChars.Contains(c) + || c == Path.AltDirectorySeparatorChar + || c == Path.DirectorySeparatorChar + || c == Path.PathSeparator + || c == '\\'); + } + + public static void Main(string[] args) + { + // Generate a long valid filename to stress the inner loop + string longName = new string('a', 5000) + ".txt"; + int iterations = 10000; + + // Warmup + IsValidFileNameOriginal(longName); + IsValidFileNamePatched(longName); + + var sw = Stopwatch.StartNew(); + for (int i = 0; i < iterations; i++) + IsValidFileNameOriginal(longName); + sw.Stop(); + long originalMs = sw.ElapsedMilliseconds; + + sw.Restart(); + for (int i = 0; i < iterations; i++) + IsValidFileNamePatched(longName); + sw.Stop(); + long patchedMs = sw.ElapsedMilliseconds; + + double ratio = (double)originalMs / Math.Max(1, patchedMs); + + Console.WriteLine($"Filename length: {longName.Length}, iterations: {iterations}"); + Console.WriteLine($"Original (GetInvalidFileNameChars per char): {originalMs} ms"); + Console.WriteLine($"Patched (static HashSet): {patchedMs} ms"); + Console.WriteLine($"Speedup ratio: {ratio:F1}x"); + + // Correctness + bool c1 = IsValidFileNameOriginal("hello.txt") == IsValidFileNamePatched("hello.txt"); + bool c2 = IsValidFileNameOriginal("bad.txt") == IsValidFileNamePatched("bad.txt"); + bool c3 = IsValidFileNameOriginal("path/sep.txt") == IsValidFileNamePatched("path/sep.txt"); + bool c4 = IsValidFileNameOriginal("ok-file_2024.pdf") == IsValidFileNamePatched("ok-file_2024.pdf"); + bool c5 = IsValidFileNameOriginal("has:colon") == IsValidFileNamePatched("has:colon"); + bool c6 = IsValidFileNameOriginal("") == IsValidFileNamePatched(""); + + bool allCorrect = c1 && c2 && c3 && c4 && c5; + Console.WriteLine($"Correctness (valid): {(c1 ? "PASS" : "FAIL")}"); + Console.WriteLine($"Correctness (angle): {(c2 ? "PASS" : "FAIL")}"); + Console.WriteLine($"Correctness (pathsep): {(c3 ? "PASS" : "FAIL")}"); + Console.WriteLine($"Correctness (dashes): {(c4 ? "PASS" : "FAIL")}"); + Console.WriteLine($"Correctness (colon): {(c5 ? "PASS" : "FAIL")}"); + + if (!allCorrect) + Environment.Exit(1); + } + } +}