What changed, and why it matters
This commit fixes a database migration for payment requests in BTCPay Server. It changes how a new 'Title' column is populated from existing JSON data and adjusts the in-code migration logic. The changes appear to be a bug fix for a migration that previously did not run correctly, rather than a deliberate security patch. There is no clear evidence of an exploitable vulnerability in the diff itself.
Review the migration for data integrity and idempotency, especially the raw SQL UPDATE statement. Ensure the migration runs safely on large PaymentRequests tables and that the change from AddColumn to raw SQL does not bypass EF migration safeguards. No immediate security patch appears required based on the diff alone.
Security signals we found
Database migration logic changed
Raw SQL used in EF migration
JSON field extraction and removal from blob column
Unit test expectations inverted (title now expected in column, not in blob)
Evidence from the diff
The commit revises the PaymentRequest title migration. The C# migration helper now extracts ‘currency’, ‘amount’, and ‘title’ unconditionally from Blob2 JSON, removing the previous separate TryMigrateTitle() method and the conditional check that required Title to be non-null before skipping migration. The EF migration is rewritten from an AddColumn + index creation to a raw SQL statement that adds the Title column and populates it from Blob2’s ‘title’ field in one step. A unit test is updated to expect the title to have been migrated into the column and removed from Blob2.
Changed components
BTCPayServer.Data/Data/PaymentRequestData.Migration.csBTCPayServer.Data/Migrations/20251216120000_pr_title.csBTCPayServer.Tests/UnitTest1.csInspect captured patch +20 / −55
diff --git a/BTCPayServer.Data/Data/PaymentRequestData.Migration.cs b/BTCPayServer.Data/Data/PaymentRequestData.Migration.cs
index b43d37f..03be676 100644
--- a/BTCPayServer.Data/Data/PaymentRequestData.Migration.cs
+++ b/BTCPayServer.Data/Data/PaymentRequestData.Migration.cs
@@ -18,7 +18,7 @@ namespace BTCPayServer.Data
public bool TryMigrate()
{
#pragma warning disable CS0618 // Type or member is obsolete
- if (Blob is (null or { Length: 0 }) && Blob2 is not null && Currency is not null && Title is not null)
+ if (Blob is (null or { Length: 0 }) && Blob2 is not null && Currency is not null)
return false;
if (Blob2 is null)
{
@@ -41,51 +41,21 @@ namespace BTCPayServer.Data
jobj.Remove("expiryDate");
Expiry = date;
}
- if (jobj["currency"] is not null)
+ Currency = jobj["currency"].Value<string>();
+ Amount = jobj["amount"] switch
{
- Currency = jobj["currency"].Value<string>();
- jobj.Remove("currency");
- }
- if (jobj["amount"] is not null)
- {
- Amount = jobj["amount"] switch
- {
- JValue jv when jv.Type == JTokenType.Float => jv.Value<decimal>(),
- JValue jv when jv.Type == JTokenType.Integer => jv.Value<long>(),
- JValue jv when jv.Type == JTokenType.String && decimal.TryParse(jv.Value<string>(), CultureInfo.InvariantCulture, out var d) => d,
- _ => 0m
- };
- jobj.Remove("amount");
- }
- Blob2 = jobj.ToString(Newtonsoft.Json.Formatting.None);
-
- // Run Title migration separately (only if Title column exists)
- try
- {
- TryMigrateTitle();
- }
- catch
- {
- // Title column doesn't exist yet - will be migrated later
- }
-
- return true;
- }
-
- public bool TryMigrateTitle()
- {
- if (Blob2 is null || Title is not null)
- return false;
-
- var jobj = JObject.Parse(Blob2);
- Title = jobj["title"]?.Value<string>();
- if (Title is not null)
+ JValue jv when jv.Type == JTokenType.Float => jv.Value<decimal>(),
+ JValue jv when jv.Type == JTokenType.Integer => jv.Value<long>(),
+ JValue jv when jv.Type == JTokenType.String && decimal.TryParse(jv.Value<string>(), CultureInfo.InvariantCulture, out var d) => d,
+ _ => 0m
+ };
+ if (jobj["title"] is not null)
{
+ Title = jobj["title"].ToString();
jobj.Remove("title");
- Blob2 = jobj.ToString(Newtonsoft.Json.Formatting.None);
- return true;
}
- return false;
+ Blob2 = jobj.ToString(Newtonsoft.Json.Formatting.None);
+ return true;
}
}
}
diff --git a/BTCPayServer.Data/Migrations/20251216120000_pr_title.cs b/BTCPayServer.Data/Migrations/20251216120000_pr_title.cs
index ee1484b..5e9be42 100644
--- a/BTCPayServer.Data/Migrations/20251216120000_pr_title.cs
+++ b/BTCPayServer.Data/Migrations/20251216120000_pr_title.cs
@@ -14,22 +14,17 @@ namespace BTCPayServer.Migrations
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
- migrationBuilder.AddColumn<string>(
- name: "Title",
- table: "PaymentRequests",
- type: "text",
- nullable: true);
-
- // Create case-insensitive index for Title searches
- migrationBuilder.Sql(@"CREATE INDEX ""IX_PaymentRequests_Title"" ON ""PaymentRequests"" (LOWER(""Title""));");
+ migrationBuilder.Sql("""
+ ALTER TABLE "PaymentRequests"
+ ADD COLUMN "Title" TEXT DEFAULT NULL;
+ UPDATE "PaymentRequests" SET "Title" = "Blob2" ->> 'title', "Blob2" = "Blob2" - 'title'
+ WHERE "Blob2" ->> 'title' IS NOT NULL;
+ """);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
- migrationBuilder.DropColumn(
- name: "Title",
- table: "PaymentRequests");
}
}
}
diff --git a/BTCPayServer.Tests/UnitTest1.cs b/BTCPayServer.Tests/UnitTest1.cs
index 6b6c3b5..04a8d32 100644
--- a/BTCPayServer.Tests/UnitTest1.cs
+++ b/BTCPayServer.Tests/UnitTest1.cs
@@ -3275,8 +3275,8 @@ namespace BTCPayServer.Tests
SELECT "Title", "Blob2" FROM "PaymentRequests"
WHERE "Id" = 'test-pr-with-title'
"""));
- Assert.Null((string)titleBeforeMigration.Title);
- Assert.Contains("\"title\"", (string)titleBeforeMigration.Blob2);
+ Assert.NotNull((string)titleBeforeMigration.Title);
+ Assert.DoesNotContain("\"title\"", (string)titleBeforeMigration.Blob2);
// Load entity through EF - this triggers TryMigrate() which calls TryMigrateTitle()
var pr = ctx.PaymentRequests.First(r => r.Id == "test-pr-with-title");
Why this scored 29/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.