From 281061372d36fb15dfdfcb9bdc2082e92a9c55e2 Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sun, 15 Feb 2026 19:37:17 +0100 Subject: [PATCH 1/4] Fixed issue 915 When trying to fill the cell of a template with a numeric value, there was a seemingly purposeless check that prevented the value to be converted to a culturally invariant string if the data was fetched from a Dictionary or a DataTable. This commit removes the check. A test and a template sample have also been added. --- .../Templates/OpenXmlTemplate.Impl.cs | 2 +- .../MiniExcelIssueTests.cs | 23 ++++++++++++++++++ tests/data/xlsx/TestIssue915.xlsx | Bin 0 -> 6209 bytes 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 tests/data/xlsx/TestIssue915.xlsx diff --git a/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs b/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs index e021acb0..9526706b 100644 --- a/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs +++ b/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs @@ -799,7 +799,7 @@ private async Task GenerateCellValuesAsync(GenerateCe else { cellValueStr = XmlHelper.EncodeXml(cellValue?.ToString()); - if (!isDictOrTable && TypeHelper.IsNumericType(type)) + if (TypeHelper.IsNumericType(type)) { if (decimal.TryParse(cellValueStr, out var decimalValue)) cellValueStr = decimalValue.ToString(CultureInfo.InvariantCulture); diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs index fd6b7e91..5d051b32 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs @@ -3784,4 +3784,27 @@ public void TestIssue888_ShouldIgnoreFrame() Assert.Equal(dataInSheet, dataRead); } + [Fact] + public void TestIssue915() + { + var templatePath = PathHelper.GetFile("xlsx/TestIssue915.xlsx"); + var value = new Dictionary + { + ["Data"] = new[] + { + new { Name = "Hill", Altitude = 6m }, + new { Name = "Mount", Altitude = 7.4m }, + new { Name = "Peak", Altitude = 8.6m } + } + }; + + using var path = AutoDeletingPath.Create(); + _excelTemplater.ApplyTemplate(path.ToString(), templatePath, value); + + var result = _excelImporter.Query(path.ToString(), true).ToList(); + + Assert.Equal(6, result[0].Altitude); + Assert.Equal(7.4, result[1].Altitude); + Assert.Equal(8.6, result[2].Altitude); + } } \ No newline at end of file diff --git a/tests/data/xlsx/TestIssue915.xlsx b/tests/data/xlsx/TestIssue915.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..476fd0efb7d5a587783cf1ffe69a0a175957ca97 GIT binary patch literal 6209 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 AZ2$lO literal 0 HcmV?d00001 From 1302899390cf928d36cedfbffc70092bad62de7d Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sun, 15 Feb 2026 19:37:59 +0100 Subject: [PATCH 2/4] Minor code cleanup --- .../Templates/OpenXmlTemplate.Impl.cs | 55 +++++++++++-------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs b/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs index 9526706b..c9807458 100644 --- a/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs +++ b/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs @@ -526,7 +526,7 @@ await writer.WriteAsync($"<{prefix}sheetData>" ProcessFormulas(rowXml, newRowIndex); await writer.WriteAsync(CleanXml(rowXml, endPrefix).ToString() #if NET5_0_OR_GREATER - .AsMemory(), cancellationToken + .AsMemory(), cancellationToken #endif ).ConfigureAwait(false); @@ -549,7 +549,7 @@ await writer.WriteAsync(CleanXml(rowXml, endPrefix).ToString() await writer.WriteAsync($"" #if NET7_0_OR_GREATER - .AsMemory(), cancellationToken + .AsMemory(), cancellationToken #endif ).ConfigureAwait(false); @@ -628,10 +628,20 @@ class GenerateCellValuesContext //todo: refactor in a way that needs less parameters [CreateSyncVersion] - private async Task GenerateCellValuesAsync(GenerateCellValuesContext generateCellValuesContext, string endPrefix, StreamWriter writer, - StringBuilder rowXml, int mergeRowCount, bool isHeaderRow, - XRowInfo rowInfo, XmlElement row, int groupingRowDiff, - string innerXml, StringBuilder outerXmlOpen, XmlElement rowElement, CancellationToken cancellationToken = default) + private async Task GenerateCellValuesAsync( + GenerateCellValuesContext generateCellValuesContext, + string endPrefix, + StreamWriter writer, + StringBuilder rowXml, + int mergeRowCount, + bool isHeaderRow, + XRowInfo rowInfo, + XmlElement row, + int groupingRowDiff, + string innerXml, + StringBuilder outerXmlOpen, + XmlElement rowElement, + CancellationToken cancellationToken = default) { var rowIndexDiff = generateCellValuesContext.rowIndexDiff; var headerDiff = generateCellValuesContext.headerDiff; @@ -698,27 +708,24 @@ private async Task GenerateCellValuesAsync(GenerateCe .Replace(")", "") .Split(' '); - object value; + object? value; if (rowInfo.IsDictionary) { - value = dict[newLines[0]]; + value = dict![newLines[0]]; } else if (rowInfo.IsDataTable) { - value = dataRow[newLines[0]]; + value = dataRow![newLines[0]]; } else { - value = string.Empty; var prop = rowInfo.PropsMap[newLines[0]]; - if (prop.PropertyInfoOrFieldInfo == PropertyInfoOrFieldInfo.PropertyInfo) + value = prop.PropertyInfoOrFieldInfo switch { - value = prop.PropertyInfo.GetValue(item); - } - else if (prop.PropertyInfoOrFieldInfo == PropertyInfoOrFieldInfo.FieldInfo) - { - value = prop.FieldInfo.GetValue(item); - } + PropertyInfoOrFieldInfo.PropertyInfo => prop.PropertyInfo.GetValue(item), + PropertyInfoOrFieldInfo.FieldInfo => prop.FieldInfo.GetValue(item), + _ => string.Empty + }; } var evaluation = EvaluateStatement(value, newLines[1], newLines[2]); @@ -750,7 +757,7 @@ private async Task GenerateCellValuesAsync(GenerateCe { var replacements = new Dictionary(); #if NETCOREAPP3_0_OR_GREATER - string MatchDelegate(Match x) => CollectionExtensions.GetValueOrDefault(replacements, x.Groups[1].Value, ""); + string MatchDelegate(Match x) => replacements.GetValueOrDefault(x.Groups[1].Value, ""); #else string MatchDelegate(Match x) => replacements.TryGetValue(x.Groups[1].Value, out var repl) ? repl : ""; #endif @@ -763,12 +770,12 @@ private async Task GenerateCellValuesAsync(GenerateCe object? cellValue; if (rowInfo.IsDictionary) { - if (!dict.TryGetValue(prop.Key, out cellValue)) + if (!dict!.TryGetValue(prop.Key, out cellValue)) continue; } else if (rowInfo.IsDataTable) { - cellValue = dataRow[prop.Key]; + cellValue = dataRow![prop.Key]; } else { @@ -782,7 +789,7 @@ private async Task GenerateCellValuesAsync(GenerateCe ? prop.Value.UnderlyingTypePropType : Nullable.GetUnderlyingType(propInfo.PropertyType) ?? propInfo.PropertyType; - string cellValueStr; + string? cellValueStr; if (type == typeof(bool)) { cellValueStr = (bool)cellValue ? "1" : "0"; @@ -791,7 +798,7 @@ private async Task GenerateCellValuesAsync(GenerateCe { cellValueStr = ConvertToDateTimeString(propInfo, cellValue); } - else if (type?.IsEnum ?? false) + else if (type?.IsEnum is true) { var description = CustomPropertyHelper.GetDescriptionAttribute(type, cellValue); cellValueStr = XmlHelper.EncodeXml(description); @@ -807,7 +814,7 @@ private async Task GenerateCellValuesAsync(GenerateCe } replacements[key] = cellValueStr; - rowXml.Replace($"@header{{{{{key}}}}}", cellValueStr); + rowXml.Replace($"@header{{{{{key}}}}}", cellValueStr ?? ""); if (isHeaderRow && row.InnerText.Contains(key)) { @@ -915,7 +922,7 @@ await writer.WriteAsync(CleanXml(newRow.OuterXml, endPrefix) } } - return new GenerateCellValuesContext() + return new GenerateCellValuesContext { currentHeader = currentHeader, headerDiff = headerDiff, From e71b0c52dd5ecad81b4de6d1ab81ab6bd311643d Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Mon, 16 Feb 2026 00:30:51 +0100 Subject: [PATCH 3/4] Changing element tag to search for formula placeholder In #917 sharedString element tags for templated data were changed from x:v to x:is but the formula placeholders did not take into account. This commit fixes that. --- src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs b/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs index c9807458..704e3b96 100644 --- a/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs +++ b/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs @@ -1074,8 +1074,8 @@ private void ProcessFormulas(StringBuilder rowXml, int rowIndex) SUM(C2:C7) - */ - var vs = c.SelectNodes("x:v", Ns); + */ + var vs = c.SelectNodes("x:is", Ns); foreach (XmlElement v in vs) { if (!v.InnerText.StartsWith("$=")) From f31bf522d7e710155d3f1d2c37080cfc6eb59bda Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Mon, 16 Feb 2026 00:31:39 +0100 Subject: [PATCH 4/4] Fixing potential formula injection vulnerability when inserting data into templates --- .../Templates/OpenXmlTemplate.Impl.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs b/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs index 704e3b96..6eca0286 100644 --- a/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs +++ b/src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs @@ -813,8 +813,14 @@ private async Task GenerateCellValuesAsync( } } - replacements[key] = cellValueStr; - rowXml.Replace($"@header{{{{{key}}}}}", cellValueStr ?? ""); + // escaping formulas + var tempReplacement = cellValueStr ?? ""; + var replacementValue = tempReplacement.StartsWith("$=") || tempReplacement.StartsWith("=") + ? $"'{tempReplacement}" + : tempReplacement; + + replacements[key] = replacementValue; + rowXml.Replace($"@header{{{{{key}}}}}", replacementValue); if (isHeaderRow && row.InnerText.Contains(key)) {