undf: assign 928-930; btcpayserver CWE-407 scan (3 defects)
btcpayserver-0001: WalletTransactionInfo.Merge Attachments.Any O(A*B) MEDIUM btcpayserver-0002: AppService gap-fill series.All O(D*S) LOW-MEDIUM btcpayserver-0003: StringExtensions.IsValidFileName GetInvalidFileNameChars O(F*I) MEDIUM
This commit is contained in:
parent
f92dc0f8aa
commit
c2a1fc15cc
7 changed files with 378 additions and 1 deletions
|
|
@ -925,5 +925,12 @@
|
||||||
"electrum-0002-0002": "UNDF-2026-000000924",
|
"electrum-0002-0002": "UNDF-2026-000000924",
|
||||||
"cake_wallet-0002-0002": "UNDF-2026-000000925",
|
"cake_wallet-0002-0002": "UNDF-2026-000000925",
|
||||||
"cake_wallet-0003-0003": "UNDF-2026-000000926",
|
"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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
24
defects/btcpayserver-0001/patch/btcpayserver-0001.patch
Normal file
24
defects/btcpayserver-0001/patch/btcpayserver-0001.patch
Normal file
|
|
@ -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<string, string>(LabelColors);
|
||||||
|
result.Attachments = new List<Attachment>(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);
|
||||||
|
}
|
||||||
100
defects/btcpayserver-0001/test/btcpayserver-0001-test.cs
Normal file
100
defects/btcpayserver-0001/test/btcpayserver-0001-test.cs
Normal file
|
|
@ -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<Attachment> MergeOriginal(List<Attachment> existing, List<Attachment> incoming)
|
||||||
|
{
|
||||||
|
var result = new List<Attachment>(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<Attachment> MergePatched(List<Attachment> existing, List<Attachment> incoming)
|
||||||
|
{
|
||||||
|
var result = new List<Attachment>(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<Attachment> MakeAttachments(int count, string prefix)
|
||||||
|
{
|
||||||
|
var list = new List<Attachment>(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
23
defects/btcpayserver-0002/patch/btcpayserver-0002.patch
Normal file
23
defects/btcpayserver-0002/patch/btcpayserver-0002.patch
Normal file
|
|
@ -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<DateTime>(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
114
defects/btcpayserver-0002/test/btcpayserver-0002-test.cs
Normal file
114
defects/btcpayserver-0002/test/btcpayserver-0002-test.cs
Normal file
|
|
@ -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<SalesItem> 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<SalesItem> series, int numberOfDays)
|
||||||
|
{
|
||||||
|
var existingDates = new HashSet<DateTime>(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<SalesItem> MakeSparse(int days, int gapEvery)
|
||||||
|
{
|
||||||
|
var list = new List<SalesItem>();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
defects/btcpayserver-0003/patch/btcpayserver-0003.patch
Normal file
26
defects/btcpayserver-0003/patch/btcpayserver-0003.patch
Normal file
|
|
@ -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<char> for O(1) lookup and zero allocation.
|
||||||
|
+ private static readonly HashSet<char> InvalidFileNameChars = new HashSet<char>(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
|
||||||
83
defects/btcpayserver-0003/test/btcpayserver-0003-test.cs
Normal file
83
defects/btcpayserver-0003/test/btcpayserver-0003-test.cs
Normal file
|
|
@ -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<char> 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<char> InvalidFileNameChars = new HashSet<char>(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<name>.txt") == IsValidFileNamePatched("bad<name>.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue