diff --git a/src/Zip/ZipHelper.cs b/src/Zip/ZipHelper.cs index 974260f..a683bb9 100644 --- a/src/Zip/ZipHelper.cs +++ b/src/Zip/ZipHelper.cs @@ -12,7 +12,17 @@ public static byte[] CreateZip(IReadOnlyList files) { foreach (var file in files) { - var entry = archive.CreateEntry(file.Name, CompressionLevel.Optimal); + // Strip any directory components so traversal sequences (e.g. "../../") + // cannot be embedded as ZIP entry names in produced archives. + var entryName = Path.GetFileName(file.Name); + if (string.IsNullOrEmpty(entryName)) + { + throw new ArgumentException( + $"File name '{file.Name}' does not resolve to a valid entry name.", + nameof(files)); + } + + var entry = archive.CreateEntry(entryName, CompressionLevel.Optimal); using var entryStream = entry.Open(); file.Content.CopyTo(entryStream); } diff --git a/tests/E4A.PostGuard.Tests/ZipHelperTests.cs b/tests/E4A.PostGuard.Tests/ZipHelperTests.cs index fa8507c..5f16746 100644 --- a/tests/E4A.PostGuard.Tests/ZipHelperTests.cs +++ b/tests/E4A.PostGuard.Tests/ZipHelperTests.cs @@ -44,10 +44,35 @@ public void MultipleFiles_AllPresentWithContent() var entries = ReadZip(zip); Assert.Equal(3, entries.Count); Assert.Equal("alpha", entries["a.txt"]); - Assert.Equal("beta", entries["dir/b.txt"]); + // Directory components are stripped, so "dir/b.txt" is stored as "b.txt". + Assert.Equal("beta", entries["b.txt"]); Assert.Equal("gamma", entries["c.bin"]); } + [Theory] + [InlineData("../../etc/passwd", "passwd")] + [InlineData("dir/nested/file.txt", "file.txt")] + [InlineData("/absolute/path.txt", "path.txt")] + public void SanitizesEntryNames_StrippingDirectoryComponents(string name, string expected) + { + var zip = ZipHelper.CreateZip([File(name, "payload")]); + + var entries = ReadZip(zip); + var entryName = Assert.Single(entries.Keys); + Assert.Equal(expected, entryName); + Assert.DoesNotContain("..", entryName); + Assert.Equal("payload", entries[entryName]); + } + + [Theory] + [InlineData("")] + [InlineData("dir/")] + [InlineData("../")] + public void ThrowsArgumentException_WhenNameHasNoFileComponent(string name) + { + Assert.Throws(() => ZipHelper.CreateZip([File(name, "payload")])); + } + [Fact] public void EmptyFileList_ProducesValidEmptyZip() {