Merge pull request #6783 from elsa-workflows/bug/zip-activity-base64-correction

Implementing better base64 evaluation + appropriate zip entry naming
This commit is contained in:
Lucas Hipólito 2025-07-10 12:51:47 +02:00 committed by GitHub
commit fd5579e551
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 98 additions and 13 deletions

View file

@ -80,7 +80,7 @@ public class CreateZipArchive : CodeActivity<Stream>
try
{
using var zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Create, leaveOpen: true);
using var zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Update, leaveOpen: true);
var entryIndex = 0;
var compressionLevel = CompressionLevel.Get(context);
@ -122,19 +122,87 @@ public class CreateZipArchive : CodeActivity<Stream>
CompressionLevel compressionLevel)
{
var binaryContent = await resolver.ResolveAsync(entryContent, context.CancellationToken);
var entryName = binaryContent.Name?.GetNameAndExtension()
var entryName = binaryContent.Name?.GetNameAndExtension()
?? string.Format(DefaultEntryNameFormat, entryIndex + 1);
// Get a unique name following Windows convention
entryName = GetUniqueEntryName(zipArchive, entryName);
var archiveEntry = zipArchive.CreateEntry(entryName, compressionLevel);
await using var entryStream = archiveEntry.Open();
await binaryContent.Stream.CopyToAsync(entryStream, context.CancellationToken);
await entryStream.FlushAsync(context.CancellationToken);
if (entryContent is not Stream)
{
await binaryContent.Stream.DisposeAsync();
}
}
private static string GetUniqueEntryName(ZipArchive zipArchive, string originalName)
{
var filenameWithoutExtension = Path.GetFileNameWithoutExtension(originalName);
var extension = Path.GetExtension(originalName);
var originalExists = false;
var highestIndex = 0;
foreach (var entry in zipArchive.Entries)
{
if (!entry.Name.Equals(originalName, StringComparison.OrdinalIgnoreCase))
{
continue;
}
originalExists = true;
var entryNameWithoutExtension = Path.GetFileNameWithoutExtension(entry.Name);
var entryExtension = Path.GetExtension(entry.Name);
// Only process entries with the same extension
if (!entryExtension.Equals(extension, StringComparison.OrdinalIgnoreCase))
continue;
// Check if this entry follows our naming pattern
highestIndex = HighestEntryNameIndex(entryNameWithoutExtension, filenameWithoutExtension, highestIndex);
}
if (!originalExists)
{
return originalName;
}
return $"{filenameWithoutExtension}({highestIndex + 1}){extension}";
}
private static int HighestEntryNameIndex(string entryNameWithoutExtension, string filenameWithoutExtension,
int highestIndex)
{
if (!entryNameWithoutExtension.StartsWith(filenameWithoutExtension, StringComparison.OrdinalIgnoreCase) ||
entryNameWithoutExtension.Length <= filenameWithoutExtension.Length ||
entryNameWithoutExtension[filenameWithoutExtension.Length] != '(')
{
return highestIndex;
}
// Extract the number between parentheses
var closingParenIndex = entryNameWithoutExtension.LastIndexOf(')');
if (closingParenIndex <= filenameWithoutExtension.Length + 1)
{
return highestIndex;
}
var indexStr = entryNameWithoutExtension.Substring(
filenameWithoutExtension.Length + 1,
closingParenIndex - filenameWithoutExtension.Length - 1);
if (int.TryParse(indexStr, out var index))
{
highestIndex = Math.Max(highestIndex, index);
}
return highestIndex;
}
}

View file

@ -96,27 +96,44 @@ public static class ContentTypeExtensions
if (s.Length % 4 != 0)
return false;
// Check valid Base64 characters
for (var i = 0; i < s.Length; i++)
// Check padding position and count
var paddingIndex = s.IndexOf('=');
switch (paddingIndex)
{
// Padding cannot be at index 0
case 0:
// Padding must be at the end
case > 0 when paddingIndex < s.Length - 2:
// All characters after first '=' must also be '='
case > 0 when s[paddingIndex..].Any(c => c != '='):
return false;
}
// Check for valid Base64 characters
for (var i = 0; i < paddingIndex; i++)
{
var c = s[i];
var isValid =
var isValid =
c is >= 'A' and <= 'Z' ||
c is >= 'a' and <= 'z' ||
c is >= '0' and <= '9' ||
c == '+' || c == '/' || c == '=';
c == '+' || c == '/';
if (!isValid)
return false;
}
// Try actual decoding and roundtrip
// Additional check for short strings that are just lowercase+numbers
// This catches "whatever" and similar false positives
if (s.Length <= 10 && s.All(c => char.IsLower(c) || char.IsDigit(c)))
return false;
// Try actual decoding
try
{
var data = Convert.FromBase64String(s);
var reEncoded = Convert.ToBase64String(data);
return s == reEncoded;
_ = Convert.FromBase64String(s);
return true;
}
catch
{