Skip to content

Commit f0a814f

Browse files
committed
Reject unsafe archive entry keys at indexing
1 parent bf4f7d3 commit f0a814f

2 files changed

Lines changed: 72 additions & 5 deletions

File tree

src/PlateauResoniteLink/Application/Importing/PlateauDatasetContentSourceFactory.cs

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,11 @@ private static async Task IndexArchiveAsync(
369369
{
370370
cancellationToken.ThrowIfCancellationRequested();
371371

372-
string entryKey = archiveFileLayoutPolicy.NormalizeRelativePath(entry.Key ?? string.Empty);
372+
if (!TryNormalizeArchiveEntryKey(entry, archiveFileLayoutPolicy, out string entryKey))
373+
{
374+
continue;
375+
}
376+
373377
if (IsSupportedArchiveFile(entryKey))
374378
{
375379
await IndexArchiveAsync(
@@ -438,10 +442,8 @@ private static async ValueTask<Stream> OpenEntryStreamAsync(
438442

439443
IArchiveEntry entry = archive.Entries.FirstOrDefault(candidate =>
440444
!candidate.IsDirectory
441-
&& string.Equals(
442-
archiveFileLayoutPolicy.NormalizeRelativePath(candidate.Key ?? string.Empty),
443-
archiveFileLayoutPolicy.NormalizeRelativePath(entryKey),
444-
StringComparison.Ordinal))
445+
&& TryNormalizeArchiveEntryKey(candidate, archiveFileLayoutPolicy, out string candidateKey)
446+
&& string.Equals(candidateKey, archiveFileLayoutPolicy.NormalizeRelativePath(entryKey), StringComparison.Ordinal))
445447
?? throw new FileNotFoundException($"The dataset entry '{entryKey}' was not found in '{archivePath}'.");
446448

447449
await using Stream entryStream = await entry.OpenEntryStreamAsync(cancellationToken);
@@ -458,5 +460,41 @@ private static bool IsSupportedArchiveFile(string path)
458460
|| string.Equals(extension, ".7z", StringComparison.OrdinalIgnoreCase);
459461
}
460462

463+
private static bool TryNormalizeArchiveEntryKey(
464+
IArchiveEntry entry,
465+
IArchiveFileLayoutPolicy archiveFileLayoutPolicy,
466+
out string entryKey)
467+
{
468+
entryKey = string.Empty;
469+
470+
if (string.IsNullOrWhiteSpace(entry.Key))
471+
{
472+
return false;
473+
}
474+
475+
string rawKey = entry.Key.Replace('\\', '/').Trim();
476+
if (rawKey.StartsWith('/')
477+
|| Path.IsPathFullyQualified(rawKey)
478+
|| (rawKey.Length >= 2 && rawKey[1] == ':'))
479+
{
480+
return false;
481+
}
482+
483+
string normalizedKey = archiveFileLayoutPolicy.NormalizeRelativePath(rawKey);
484+
if (normalizedKey.Length == 0)
485+
{
486+
return false;
487+
}
488+
489+
string[] segments = normalizedKey.Split('/', StringSplitOptions.RemoveEmptyEntries);
490+
if (segments.Any(static segment => segment is "." or ".."))
491+
{
492+
return false;
493+
}
494+
495+
entryKey = normalizedKey;
496+
return true;
497+
}
498+
461499
}
462500
}

tests/PlateauResoniteLink.Tests/Sources/ArchivePlateauDatasetContentSourceFactoryTests.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.IO;
44
using System.IO.Compression;
5+
using System.Linq;
56
using System.Text;
67
using System.Threading.Tasks;
78

@@ -99,6 +100,34 @@ public async Task EnsureLocalFileAsyncUsesDistinctCacheDirectoriesForSameNamedAr
99100
Assert.NotEqual(Path.GetDirectoryName(firstLocalFilePath), Path.GetDirectoryName(secondLocalFilePath));
100101
}
101102

103+
[Fact]
104+
public async Task CreateAsyncIndexesOnlyArchiveEntriesWithSafeRelativePaths()
105+
{
106+
byte[] archiveBytes = CreateZipArchive(
107+
("udx/bldg/area/plateau_tokyo23ku_bldg_533944.gml", "<CityModel />"),
108+
("../../outside.txt", "escape"),
109+
("/absolute.txt", "absolute"),
110+
("C:/outside/windows-absolute.txt", "drive"),
111+
("udx/./texture.png", "dot"));
112+
113+
using TemporaryDirectory workRoot = new();
114+
string archivePath = Path.Combine(workRoot.Path, "malicious.zip");
115+
await File.WriteAllBytesAsync(archivePath, archiveBytes);
116+
117+
IPlateauDatasetContentSource datasetSource = await PlateauDatasetContentSourceFactory.CreateAsync(
118+
archivePath,
119+
new RemoteArchiveDistributionPolicy(),
120+
new ArchiveFileLayoutPolicy());
121+
122+
string[] files = datasetSource.EnumerateFiles().ToArray();
123+
124+
Assert.Equal(["udx/bldg/area/plateau_tokyo23ku_bldg_533944.gml"], files);
125+
Assert.False(datasetSource.FileExists("outside.txt"));
126+
Assert.False(datasetSource.FileExists("absolute.txt"));
127+
Assert.False(datasetSource.FileExists("C:/outside/windows-absolute.txt"));
128+
Assert.False(datasetSource.FileExists("udx/texture.png"));
129+
}
130+
102131
[Fact]
103132
public async Task ResolveRelativePathUsesConfiguredArchiveFileLayoutPolicy()
104133
{

0 commit comments

Comments
 (0)