Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 41 additions & 28 deletions src/MiniExcel.OpenXml/Templates/OpenXmlTemplate.Impl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -549,7 +549,7 @@ await writer.WriteAsync(CleanXml(rowXml, endPrefix).ToString()

await writer.WriteAsync($"</{prefix}sheetData>"
#if NET7_0_OR_GREATER
.AsMemory(), cancellationToken
.AsMemory(), cancellationToken
#endif
).ConfigureAwait(false);

Expand Down Expand Up @@ -628,10 +628,20 @@ class GenerateCellValuesContext

//todo: refactor in a way that needs less parameters
[CreateSyncVersion]
private async Task<GenerateCellValuesContext> 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<GenerateCellValuesContext> 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;
Expand Down Expand Up @@ -698,27 +708,24 @@ private async Task<GenerateCellValuesContext> 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]);
Expand Down Expand Up @@ -750,7 +757,7 @@ private async Task<GenerateCellValuesContext> GenerateCellValuesAsync(GenerateCe
{
var replacements = new Dictionary<string, string>();
#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, "");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

This line retrieves user-provided values for template replacement. If these values start with $=, they will be interpreted as formulas in the resulting Excel file, leading to Formula Injection. See the comment on line 800 for more details and remediation steps.

#else
string MatchDelegate(Match x) => replacements.TryGetValue(x.Groups[1].Value, out var repl) ? repl : "";
#endif
Expand All @@ -763,12 +770,12 @@ private async Task<GenerateCellValuesContext> 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
{
Expand All @@ -782,7 +789,7 @@ private async Task<GenerateCellValuesContext> GenerateCellValuesAsync(GenerateCe
? prop.Value.UnderlyingTypePropType
: Nullable.GetUnderlyingType(propInfo.PropertyType) ?? propInfo.PropertyType;

string cellValueStr;
string? cellValueStr;
if (type == typeof(bool))
{
cellValueStr = (bool)cellValue ? "1" : "0";
Expand All @@ -791,23 +798,29 @@ private async Task<GenerateCellValuesContext> 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);
}
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);
}
}

replacements[key] = cellValueStr;
rowXml.Replace($"@header{{{{{key}}}}}", cellValueStr);
// escaping formulas
var tempReplacement = cellValueStr ?? "";
var replacementValue = tempReplacement.StartsWith("$=") || tempReplacement.StartsWith("=")
? $"&apos;{tempReplacement}"
: tempReplacement;

replacements[key] = replacementValue;
rowXml.Replace($"@header{{{{{key}}}}}", replacementValue);

if (isHeaderRow && row.InnerText.Contains(key))
{
Expand Down Expand Up @@ -915,7 +928,7 @@ await writer.WriteAsync(CleanXml(newRow.OuterXml, endPrefix)
}
}

return new GenerateCellValuesContext()
return new GenerateCellValuesContext
{
currentHeader = currentHeader,
headerDiff = headerDiff,
Expand Down Expand Up @@ -1067,8 +1080,8 @@ private void ProcessFormulas(StringBuilder rowXml, int rowIndex)
<c r="C8" s="3">
<f>SUM(C2:C7)</f>
</c>
*/
var vs = c.SelectNodes("x:v", Ns);
*/
var vs = c.SelectNodes("x:is", Ns);
foreach (XmlElement v in vs)
{
if (!v.InnerText.StartsWith("$="))
Expand Down
23 changes: 23 additions & 0 deletions tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string,object>
{
["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);
Comment on lines +3806 to +3808
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For consistency and clarity in the test, it would be better to use floating-point literals for all assertions. The values read from Excel numeric cells are of type double. The current assertions mix an int literal (6) with double literals (7.4, 8.6), which is inconsistent. Using double literals for all assertions makes the expected type explicit.

        Assert.Equal(6.0, result[0].Altitude);
        Assert.Equal(7.4, result[1].Altitude);
        Assert.Equal(8.6, result[2].Altitude);

}
}
Binary file added tests/data/xlsx/TestIssue915.xlsx
Binary file not shown.
Loading