Skip to content

Commit 0e80f87

Browse files
authored
Harden validation of package metadata (#10778)
1 parent 4c8ecb1 commit 0e80f87

36 files changed

Lines changed: 1210 additions & 165 deletions

src/Catalog/CatalogCommitItem.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
@@ -40,6 +40,12 @@ public CatalogCommitItem(
4040
Types = types;
4141
TypeUris = typeUris;
4242

43+
Utils.AssertValidPackageId(packageIdentity.Id);
44+
if (packageIdentity.Version is null)
45+
{
46+
throw new ArgumentNullException("The version in the package identity must not be null.");
47+
}
48+
4349
IsPackageDetails = HasTypeUri(Schema.DataTypes.PackageDetails);
4450
IsPackageDelete = HasTypeUri(Schema.DataTypes.PackageDelete);
4551
}
@@ -123,4 +129,4 @@ private static IEnumerable<string> GetTypes(JObject commitItem)
123129
}
124130
}
125131
}
126-
}
132+
}

src/Catalog/CatalogIndexEntry.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
@@ -136,8 +136,15 @@ private void Initialize(
136136
throw new ArgumentNullException(nameof(packageIdentity));
137137
}
138138

139+
if (packageIdentity.Version is null)
140+
{
141+
throw new ArgumentNullException("The ID and version of the package identity must not be null.");
142+
}
143+
144+
Utils.AssertValidPackageId(packageIdentity.Id);
145+
139146
Id = packageIdentity.Id;
140147
Version = packageIdentity.Version;
141148
}
142149
}
143-
}
150+
}

src/Catalog/Dnx/DnxCatalogCollector.cs

Lines changed: 9 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using System.Threading.Tasks;
1414
using Microsoft.Extensions.Logging;
1515
using Newtonsoft.Json.Linq;
16+
using NuGet.Packaging;
1617
using NuGet.Protocol.Catalog;
1718
using NuGet.Services.Metadata.Catalog.Helpers;
1819
using NuGet.Services.Metadata.Catalog.Persistence;
@@ -456,35 +457,18 @@ private async Task<string> GetNuspecAsync(
456457

457458
private static string GetNuspec(Stream stream, string id)
458459
{
459-
string name = $"{id}.nuspec";
460+
using var archive = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen: true);
461+
using var packageArchiveReader = new PackageArchiveReader(archive);
460462

461-
using (var archive = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen: true))
463+
var actualId = packageArchiveReader.GetIdentity().Id;
464+
if (!StringComparer.OrdinalIgnoreCase.Equals(id, actualId))
462465
{
463-
// first look for a nuspec file named as the package id
464-
foreach (ZipArchiveEntry entry in archive.Entries)
465-
{
466-
if (entry.FullName.Equals(name, StringComparison.InvariantCultureIgnoreCase))
467-
{
468-
using (TextReader reader = new StreamReader(entry.Open()))
469-
{
470-
return reader.ReadToEnd();
471-
}
472-
}
473-
}
474-
// failing that, just return the first file that appears to be a nuspec
475-
foreach (ZipArchiveEntry entry in archive.Entries)
476-
{
477-
if (entry.FullName.EndsWith(".nuspec", StringComparison.InvariantCultureIgnoreCase))
478-
{
479-
using (TextReader reader = new StreamReader(entry.Open()))
480-
{
481-
return reader.ReadToEnd();
482-
}
483-
}
484-
}
466+
throw new InvalidOperationException($"The package ID in the catalog leaf '{id}' does not match the package ID in the .nupkg '{actualId}'.");
485467
}
486468

487-
return null;
469+
using var nuspecStream = packageArchiveReader.GetNuspec();
470+
using var nuspecReader = new StreamReader(nuspecStream);
471+
return nuspecReader.ReadToEnd();
488472
}
489473

490474
private static Dictionary<string, string> GetTelemetryProperties(CatalogEntry catalogEntry)

src/Catalog/FlatContainerPackagePathProvider.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
@@ -27,6 +27,9 @@ public string GetPackagePath(string id, string version)
2727
throw new ArgumentNullException(nameof(version));
2828
}
2929

30+
Utils.AssertValidPackageId(id);
31+
Utils.AssertValidPackageVersion(version);
32+
3033
var idLowerCase = id.ToLowerInvariant();
3134
var versionLowerCase = NuGetVersionUtility.NormalizeVersion(version).ToLowerInvariant();
3235
var packageFileName = PackageUtility.GetPackageFileName(idLowerCase, versionLowerCase);
@@ -51,6 +54,9 @@ public string GetIconPath(string id, string version, bool normalize)
5154
throw new ArgumentNullException(nameof(version));
5255
}
5356

57+
Utils.AssertValidPackageId(id);
58+
Utils.AssertValidPackageVersion(version);
59+
5460
var idLowerCase = id.ToLowerInvariant();
5561

5662
string versionLowerCase;

src/Catalog/Helpers/NuGetVersionUtility.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System;
45
using NuGet.Versioning;
56

67
namespace NuGet.Services.Metadata.Catalog.Helpers
@@ -12,7 +13,7 @@ public static string NormalizeVersion(string version)
1213
NuGetVersion parsedVersion;
1314
if (!NuGetVersion.TryParse(version, out parsedVersion))
1415
{
15-
return version;
16+
throw new InvalidOperationException($"Invalid version found when normalized: '{version}'");
1617
}
1718

1819
return parsedVersion.ToNormalizedString();

src/Catalog/Helpers/PackageUtility.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
@@ -19,7 +19,10 @@ public static string GetPackageFileName(string packageId, string packageVersion)
1919
throw new ArgumentException(Strings.ArgumentMustNotBeNullOrEmpty, nameof(packageVersion));
2020
}
2121

22+
Utils.AssertValidPackageId(packageId);
23+
Utils.AssertValidPackageVersion(packageVersion);
24+
2225
return $"{packageId}.{packageVersion}.nupkg";
2326
}
2427
}
25-
}
28+
}

src/Catalog/Helpers/Utils.cs

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,15 @@
1414
using System.Xml;
1515
using System.Xml.Linq;
1616
using System.Xml.Xsl;
17-
using Newtonsoft.Json;
1817
using Newtonsoft.Json.Linq;
19-
using NuGet.Services.Metadata.Catalog.Helpers;
18+
using NuGet.Packaging;
19+
using NuGet.Versioning;
20+
2021
#if NETFRAMEWORK
2122
using JsonLD.Core;
23+
using Newtonsoft.Json;
2224
using NuGet.Services.Metadata.Catalog.JsonLDIntegration;
25+
using NuGet.Services.Metadata.Catalog.Helpers;
2326
using VDS.RDF;
2427
using VDS.RDF.Parsing;
2528
#endif
@@ -28,16 +31,16 @@ namespace NuGet.Services.Metadata.Catalog
2831
{
2932
public static class Utils
3033
{
34+
#if NETFRAMEWORK
3135
private const string XslTransformNuSpec = "xslt.nuspec.xslt";
3236
private const string XslTransformNormalizeNuSpecNamespace = "xslt.normalizeNuspecNamespace.xslt";
3337

3438
private static readonly Lazy<XslCompiledTransform> XslTransformNuSpecCache = new Lazy<XslCompiledTransform>(() => SafeLoadXslTransform(XslTransformNuSpec));
3539
private static readonly Lazy<XslCompiledTransform> XslTransformNormalizeNuSpecNamespaceCache = new Lazy<XslCompiledTransform>(() => SafeLoadXslTransform(XslTransformNormalizeNuSpecNamespace));
40+
#endif
3641

3742
private static readonly char[] TagTrimChars = { ',', ' ', '\t', '|', ';' };
3843

39-
private static readonly char[] Slashes = { '/', '\\' };
40-
4144
public static string[] SplitTags(string original)
4245
{
4346
var fields = original
@@ -49,6 +52,22 @@ public static string[] SplitTags(string original)
4952
return fields;
5053
}
5154

55+
public static void AssertValidPackageId(string packageId)
56+
{
57+
if (packageId is null || !PackageIdValidator.IsValidPackageId(packageId))
58+
{
59+
throw new InvalidOperationException($"The package ID {(packageId is null ? "<null>" : $"'{packageId}'")} is not valid.");
60+
}
61+
}
62+
63+
public static void AssertValidPackageVersion(string packageVersion)
64+
{
65+
if (packageVersion is null || !NuGetVersion.TryParse(packageVersion, out _))
66+
{
67+
throw new InvalidOperationException($"The package version {(packageVersion is null ? "<null>" : $"'{packageVersion}'")} is not valid.");
68+
}
69+
}
70+
5271
public static Stream GetResourceStream(string resourceName)
5372
{
5473
if (string.IsNullOrEmpty(resourceName))
@@ -114,20 +133,10 @@ private static XslCompiledTransform SafeLoadXslTransform(string resourceName)
114133
return transform;
115134
}
116135

117-
public static XDocument GetNuspec(ZipArchive package)
136+
public static XDocument GetNuspecXDocument(PackageArchiveReader packageArchiveReader)
118137
{
119-
if (package == null) { return null; }
120-
121-
foreach (ZipArchiveEntry part in package.Entries)
122-
{
123-
if (part.FullName.EndsWith(".nuspec") && part.FullName.IndexOfAny(Slashes) == -1)
124-
{
125-
XDocument nuspec = XDocument.Load(part.Open());
126-
return nuspec;
127-
}
128-
}
129-
130-
return null;
138+
using var nuspecStream = packageArchiveReader.GetNuspec();
139+
return XDocument.Load(nuspecStream);
131140
}
132141

133142
public static Uri Expand(JToken context, string term)
@@ -203,14 +212,17 @@ public static NupkgMetadata GetNupkgMetadata(Stream stream, string packageHash)
203212
stream.Seek(0, SeekOrigin.Begin);
204213

205214
using (var package = new ZipArchive(stream, ZipArchiveMode.Read, leaveOpen: true))
215+
using (var packageArchiveReader = new PackageArchiveReader(package))
206216
{
207-
var nuspec = GetNuspec(package);
208-
209-
if (nuspec == null)
217+
var identity = packageArchiveReader.GetIdentity();
218+
AssertValidPackageId(identity.Id);
219+
if (identity.Version is null)
210220
{
211-
throw new InvalidDataException("Unable to find nuspec");
221+
throw new InvalidOperationException("The version from the package identity must not be null.");
212222
}
213223

224+
var nuspec = GetNuspecXDocument(packageArchiveReader);
225+
214226
var entries = GetEntries(package);
215227

216228
return new NupkgMetadata(nuspec, entries, packageSize, packageHash);

src/Catalog/Helpers/XsltHelper.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System.Xml;
55
using System.Xml.XPath;
6+
using NuGet.Packaging;
67
using NuGet.Services.Metadata.Catalog.Helpers;
78
using NuGet.Versioning;
89

@@ -40,6 +41,16 @@ public string LowerCase(string original)
4041
return original.ToLowerInvariant();
4142
}
4243

44+
public bool IsValidPackageId(string original)
45+
{
46+
return PackageIdValidator.IsValidPackageId(original);
47+
}
48+
49+
public bool IsValidVersion(string original)
50+
{
51+
return NuGetVersion.TryParse(original, out _);
52+
}
53+
4354
public string NormalizeVersion(string original)
4455
{
4556
return NuGetVersionUtility.NormalizeVersion(original);

src/Catalog/Persistence/AzureStorage.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -438,13 +438,7 @@ protected override async Task OnDeleteAsync(Uri resourceUri, DeleteRequestOption
438438
public override Uri GetUri(string name)
439439
{
440440
var baseUri = RemoveQueryString(_directory.Uri);
441-
442-
if (baseUri.EndsWith("/"))
443-
{
444-
return new Uri($"{baseUri}{name}", UriKind.Absolute);
445-
}
446-
447-
return new Uri($"{baseUri}/{name}", UriKind.Absolute);
441+
return new Uri(baseUri.TrimEnd('/') + '/' + name.TrimStart('/'), UriKind.Absolute);
448442
}
449443

450444
public override async Task<bool> AreSynchronized(Uri firstResourceUri, Uri secondResourceUri)

src/Catalog/Persistence/Storage.cs

Lines changed: 2 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ public virtual Task<bool> UpdateCacheControlAsync(Uri resourceUri, string cacheC
181181

182182
public Uri ResolveUri(string relativeUri)
183183
{
184-
return new Uri(BaseAddress, relativeUri);
184+
return Services.Storage.Storage.ResolveUri(BaseAddress, relativeUri);
185185
}
186186

187187
/// <summary>
@@ -197,39 +197,7 @@ public virtual Task<bool> AreSynchronized(Uri firstResourceUri, Uri secondResour
197197

198198
public string GetName(Uri uri)
199199
{
200-
if (uri is null)
201-
{
202-
throw new ArgumentNullException(nameof(uri));
203-
}
204-
205-
if (BaseAddress is null)
206-
{
207-
throw new InvalidOperationException("BaseAddress must be set.");
208-
}
209-
210-
// The GetLeftPart method performs encoding under the hood, which could be problematic if it contains Unicode characters.
211-
// It doesn't perform double encoding; the Uri object knows if it's already encoded and skips encoding it again.
212-
// Decoding the base address to remove any encoded characters.
213-
string address = Uri.UnescapeDataString(BaseAddress.GetLeftPart(UriPartial.Path)); // Remove potential query or SAS from the URI
214-
if (!address.EndsWith("/"))
215-
{
216-
address += "/";
217-
}
218-
219-
// Do the same with the above to get it decoded.
220-
string fullPath = Uri.UnescapeDataString(uri.GetLeftPart(UriPartial.Path)); // Remove potential query or SAS from the URI
221-
222-
// handle mismatched scheme (http vs https)
223-
int schemeLengthDifference = uri.Scheme.Length - BaseAddress.Scheme.Length;
224-
225-
string name = fullPath.Substring(address.Length + schemeLengthDifference);
226-
227-
if (name.Contains("#"))
228-
{
229-
name = name.Substring(0, name.IndexOf("#"));
230-
}
231-
232-
return name;
200+
return Services.Storage.Storage.GetName(BaseAddress, uri);
233201
}
234202

235203
public virtual Uri GetUri(string name)

0 commit comments

Comments
 (0)