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
17 changes: 17 additions & 0 deletions DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,15 @@ part name on each pack run; the normalizer rewrites that part to
matching content-type and relationship references, and gives ZIP entries stable
timestamps. This is the package reproducibility boundary for `.nupkg` and
`.snupkg` archives (#2756).
Before rewriting, the normalizer rejects packages with more than 4096 ZIP
entries, any entry above 128 MiB uncompressed, total uncompressed content above
512 MiB, or XML reference text above 16 MiB so crafted packages cannot force
unbounded normalization work (#2892).
It also rejects unsafe ZIP entry names before creating the destination archive:
absolute paths, Windows drive roots, backslash separators, empty path segments,
parent-directory segments, empty normalized names, and destination names that
collide after path normalization are not preserved into normalized packages
(#2894).

When you intentionally update a dependency (or add a new direct `PackageReference`), regenerate the lock files locally and commit the diff in the same change:

Expand Down Expand Up @@ -2048,6 +2057,14 @@ release の `dotnet publish`(RID ごと)と `dotnet pack`(NuGet パッケ
`package/services/metadata/core-properties/core-properties.psmdcp` に書き換え、
対応する content-type / relationship 参照も更新し、ZIP entry timestamp を固定します。
これが `.nupkg` / `.snupkg` archive の package 再現性境界です (#2756)。
書き換え前に、normalizer は 4096 を超える ZIP entry、128 MiB を超える
uncompressed entry、512 MiB を超える合計 uncompressed content、または
16 MiB を超える XML 参照テキストを持つ package を拒否し、細工された
package が無制限の normalize 作業を強制できないようにします (#2892)。
また destination archive を作る前に unsafe な ZIP entry 名も拒否します。
absolute path、Windows drive root、backslash separator、空の path segment、
parent-directory segment、空に正規化される名前、path 正規化後に衝突する
destination 名は、normalized package に保持されません (#2894)。

依存を意図的に更新する(あるいは直接 `PackageReference` を追加する)場合は、ローカルで lock ファイルを再生成し、同じ変更でコミットしてください:

Expand Down
18 changes: 18 additions & 0 deletions changelog.d/unreleased/2892.security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
category: security
issues:
- 2892
affected:
- tools/CodeIndex.PackageNormalize/CodeIndex.PackageNormalize.csproj
- tools/CodeIndex.PackageNormalize/PackageNormalizeCli.cs
- tests/CodeIndex.Tests/ReleaseWorkflowTests.cs
- DEVELOPER_GUIDE.md
---

## English

- **Package normalization now caps ZIP resource usage (#2892)** — `CodeIndex.PackageNormalize` now rejects packages that exceed documented ZIP entry-count, per-entry, total-uncompressed, or XML-text limits before rewriting release artifacts.

## 日本語

- **Package normalize が ZIP resource 使用量を上限で制限するようになりました (#2892)** — `CodeIndex.PackageNormalize` は release artifact を書き換える前に、文書化された ZIP entry 数、entry 単位、合計 uncompressed size、XML text の上限を超える package を拒否します。
17 changes: 17 additions & 0 deletions changelog.d/unreleased/2894.security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
category: security
issues:
- 2894
affected:
- tools/CodeIndex.PackageNormalize/PackageNormalizeCli.cs
- tests/CodeIndex.Tests/ReleaseWorkflowTests.cs
- DEVELOPER_GUIDE.md
---

## English

- **Package normalization now rejects unsafe ZIP entry names (#2894)** — `CodeIndex.PackageNormalize` validates source and destination ZIP entry names before rewriting, preventing absolute paths, parent traversal, backslash separators, empty path segments, and normalized duplicate destination names from being preserved.

## 日本語

- **Package normalize が unsafe な ZIP entry 名を拒否するようになりました (#2894)** — `CodeIndex.PackageNormalize` は書き換え前に source / destination の ZIP entry 名を検証し、absolute path、parent traversal、backslash separator、空の path segment、正規化後に重複する destination 名が保持されないようにします。
173 changes: 173 additions & 0 deletions tests/CodeIndex.Tests/ReleaseWorkflowTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,172 @@ public void PackageNormalizer_RewritesRandomCorePropertiesPartDeterministically(
}
}

[Fact]
public void PackageNormalizer_RejectsPackageThatExceedsEntryCountLimit()
{
var projectRoot = TestProjectHelper.CreateTempProject(nameof(PackageNormalizer_RejectsPackageThatExceedsEntryCountLimit));
try
{
var packagePath = Path.Combine(projectRoot, "too-many-entries.nupkg");
CreatePackageWithEntries(
packagePath,
("package/services/metadata/core-properties/random.psmdcp", ""),
("payload.txt", "ok"));

var limits = PackageNormalizeLimits.Default with { MaxEntryCount = 1 };

var exception = Assert.Throws<InvalidOperationException>(() => PackageCorePropertiesNormalizer.NormalizePackage(packagePath, limits));
Assert.Contains("2 ZIP entries", exception.Message);
Assert.Contains("limit of 1", exception.Message);
Assert.False(File.Exists(packagePath + ".normalize-tmp"));
}
finally
{
TestProjectHelper.DeleteDirectory(projectRoot);
}
}

[Fact]
public void PackageNormalizer_RejectsEntryThatExceedsPerEntryLimit()
{
var projectRoot = TestProjectHelper.CreateTempProject(nameof(PackageNormalizer_RejectsEntryThatExceedsPerEntryLimit));
try
{
var packagePath = Path.Combine(projectRoot, "large-entry.nupkg");
CreatePackageWithEntries(
packagePath,
("package/services/metadata/core-properties/random.psmdcp", ""),
("payload.bin", "123456"));

var limits = PackageNormalizeLimits.Default with
{
MaxEntryUncompressedBytes = 5,
MaxTotalUncompressedBytes = 100,
};

var exception = Assert.Throws<InvalidOperationException>(() => PackageCorePropertiesNormalizer.NormalizePackage(packagePath, limits));
Assert.Contains("payload.bin", exception.Message);
Assert.Contains("per-entry limit of 5 bytes", exception.Message);
Assert.False(File.Exists(packagePath + ".normalize-tmp"));
}
finally
{
TestProjectHelper.DeleteDirectory(projectRoot);
}
}

[Fact]
public void PackageNormalizer_RejectsPackageThatExceedsTotalUncompressedLimit()
{
var projectRoot = TestProjectHelper.CreateTempProject(nameof(PackageNormalizer_RejectsPackageThatExceedsTotalUncompressedLimit));
try
{
var packagePath = Path.Combine(projectRoot, "large-total.nupkg");
CreatePackageWithEntries(
packagePath,
("package/services/metadata/core-properties/random.psmdcp", ""),
("a.txt", "1234"),
("b.txt", "5678"));

var limits = PackageNormalizeLimits.Default with
{
MaxEntryUncompressedBytes = 10,
MaxTotalUncompressedBytes = 6,
};

var exception = Assert.Throws<InvalidOperationException>(() => PackageCorePropertiesNormalizer.NormalizePackage(packagePath, limits));
Assert.Contains("b.txt", exception.Message);
Assert.Contains("uncompressed size exceed the limit of 6 bytes", exception.Message);
Assert.False(File.Exists(packagePath + ".normalize-tmp"));
}
finally
{
TestProjectHelper.DeleteDirectory(projectRoot);
}
}

[Fact]
public void PackageNormalizer_RejectsXmlEntryThatExceedsTextLimit()
{
var projectRoot = TestProjectHelper.CreateTempProject(nameof(PackageNormalizer_RejectsXmlEntryThatExceedsTextLimit));
try
{
var packagePath = Path.Combine(projectRoot, "large-xml.nupkg");
CreatePackageWithEntries(
packagePath,
("package/services/metadata/core-properties/random.psmdcp", ""),
("[Content_Types].xml", "123456"));

var limits = PackageNormalizeLimits.Default with
{
MaxEntryUncompressedBytes = 100,
MaxTotalUncompressedBytes = 100,
MaxXmlTextChars = 5,
};

var exception = Assert.Throws<InvalidOperationException>(() => PackageCorePropertiesNormalizer.NormalizePackage(packagePath, limits));
Assert.Contains("[Content_Types].xml", exception.Message);
Assert.Contains("text limit of 5 characters", exception.Message);
}
finally
{
TestProjectHelper.DeleteDirectory(projectRoot);
}
}

[Theory]
[InlineData("/payload.txt", "must be a relative path")]
[InlineData("C:/payload.txt", "must be a relative path")]
[InlineData("./C:/payload.txt", "must be a relative path")]
[InlineData("../payload.txt", "must not contain parent-directory segments")]
[InlineData("folder\\payload.txt", "must use '/' separators")]
[InlineData("folder//payload.txt", "must not contain empty path segments")]
public void PackageNormalizer_RejectsUnsafeZipEntryNames(string unsafeEntryName, string expectedMessage)
{
var projectRoot = TestProjectHelper.CreateTempProject(nameof(PackageNormalizer_RejectsUnsafeZipEntryNames));
try
{
var packagePath = Path.Combine(projectRoot, "unsafe-name.nupkg");
CreatePackageWithEntries(
packagePath,
("package/services/metadata/core-properties/random.psmdcp", ""),
(unsafeEntryName, "payload"));

var exception = Assert.Throws<InvalidOperationException>(() => PackageCorePropertiesNormalizer.NormalizePackage(packagePath));
Assert.Contains(unsafeEntryName, exception.Message);
Assert.Contains(expectedMessage, exception.Message);
Assert.False(File.Exists(packagePath + ".normalize-tmp"));
}
finally
{
TestProjectHelper.DeleteDirectory(projectRoot);
}
}

[Fact]
public void PackageNormalizer_RejectsDestinationNamesThatNormalizeToDuplicates()
{
var projectRoot = TestProjectHelper.CreateTempProject(nameof(PackageNormalizer_RejectsDestinationNamesThatNormalizeToDuplicates));
try
{
var packagePath = Path.Combine(projectRoot, "duplicate-normalized-name.nupkg");
CreatePackageWithEntries(
packagePath,
("package/services/metadata/core-properties/random.psmdcp", ""),
("docs/readme.txt", "one"),
("docs/./readme.txt", "two"));

var exception = Assert.Throws<InvalidOperationException>(() => PackageCorePropertiesNormalizer.NormalizePackage(packagePath));
Assert.Contains("docs/./readme.txt", exception.Message);
Assert.Contains("duplicate destination name docs/readme.txt", exception.Message);
Assert.False(File.Exists(packagePath + ".normalize-tmp"));
}
finally
{
TestProjectHelper.DeleteDirectory(projectRoot);
}
}

[Fact]
public void ReleaseWorkflow_PublishesOfficialContainerImage()
{
Expand Down Expand Up @@ -212,6 +378,13 @@ private static void CreateMinimalNuGetPackage(string packagePath, string corePro
""");
}

private static void CreatePackageWithEntries(string packagePath, params (string EntryName, string Content)[] entries)
{
using var archive = ZipFile.Open(packagePath, ZipArchiveMode.Create);
foreach (var entry in entries)
WriteZipEntry(archive, entry.EntryName, entry.Content);
}

private static void WriteZipEntry(ZipArchive archive, string entryName, string content)
{
var entry = archive.CreateEntry(entryName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<InternalsVisibleTo Include="CodeIndex.Tests" />
</ItemGroup>

</Project>
Loading
Loading