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",
|
||||
"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"
|
||||
}
|
||||
|
|
|
|||
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