Skip to content

Commit 298c04e

Browse files
authored
Skip directory symlink recursion on TarFile archive creation (#74376)
* Skip directory symlink recursion on TarFile archive creation * Add symlink verification * Address suggestions by danmoseley Co-authored-by: carlossanlop <[email protected]>
1 parent d80d71f commit 298c04e

File tree

3 files changed

+157
-2
lines changed

3 files changed

+157
-2
lines changed

src/libraries/System.Formats.Tar/src/System/Formats/Tar/TarFile.cs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.Diagnostics;
77
using System.IO;
8+
using System.IO.Enumeration;
89
using System.Threading;
910
using System.Threading.Tasks;
1011

@@ -278,12 +279,20 @@ private static void CreateFromDirectoryInternal(string sourceDirectoryName, Stre
278279
DirectoryInfo di = new(sourceDirectoryName);
279280
string basePath = GetBasePathForCreateFromDirectory(di, includeBaseDirectory);
280281

282+
bool skipBaseDirRecursion = false;
281283
if (includeBaseDirectory)
282284
{
283285
writer.WriteEntry(di.FullName, GetEntryNameForBaseDirectory(di.Name));
286+
skipBaseDirRecursion = (di.Attributes & FileAttributes.ReparsePoint) != 0;
284287
}
285288

286-
foreach (FileSystemInfo file in di.EnumerateFileSystemInfos("*", SearchOption.AllDirectories))
289+
if (skipBaseDirRecursion)
290+
{
291+
// The base directory is a symlink, do not recurse into it
292+
return;
293+
}
294+
295+
foreach (FileSystemInfo file in GetFileSystemEnumerationForCreation(sourceDirectoryName))
287296
{
288297
writer.WriteEntry(file.FullName, GetEntryNameForFileSystemInfo(file, basePath.Length));
289298
}
@@ -325,18 +334,44 @@ private static async Task CreateFromDirectoryInternalAsync(string sourceDirector
325334
DirectoryInfo di = new(sourceDirectoryName);
326335
string basePath = GetBasePathForCreateFromDirectory(di, includeBaseDirectory);
327336

337+
bool skipBaseDirRecursion = false;
328338
if (includeBaseDirectory)
329339
{
330340
await writer.WriteEntryAsync(di.FullName, GetEntryNameForBaseDirectory(di.Name), cancellationToken).ConfigureAwait(false);
341+
skipBaseDirRecursion = (di.Attributes & FileAttributes.ReparsePoint) != 0;
331342
}
332343

333-
foreach (FileSystemInfo file in di.EnumerateFileSystemInfos("*", SearchOption.AllDirectories))
344+
if (skipBaseDirRecursion)
345+
{
346+
// The base directory is a symlink, do not recurse into it
347+
return;
348+
}
349+
350+
foreach (FileSystemInfo file in GetFileSystemEnumerationForCreation(sourceDirectoryName))
334351
{
335352
await writer.WriteEntryAsync(file.FullName, GetEntryNameForFileSystemInfo(file, basePath.Length), cancellationToken).ConfigureAwait(false);
336353
}
337354
}
338355
}
339356

357+
// Generates a recursive enumeration of the filesystem entries inside the specified source directory, while
358+
// making sure that directory symlinks do not get recursed.
359+
private static IEnumerable<FileSystemInfo> GetFileSystemEnumerationForCreation(string sourceDirectoryName)
360+
{
361+
return new FileSystemEnumerable<FileSystemInfo>(
362+
directory: sourceDirectoryName,
363+
transform: (ref FileSystemEntry entry) => entry.ToFileSystemInfo(),
364+
options: new EnumerationOptions()
365+
{
366+
RecurseSubdirectories = true
367+
})
368+
{
369+
ShouldRecursePredicate = IsNotADirectorySymlink
370+
};
371+
372+
static bool IsNotADirectorySymlink(ref FileSystemEntry entry) => entry.IsDirectory && (entry.Attributes & FileAttributes.ReparsePoint) == 0;
373+
}
374+
340375
// Determines what should be the base path for all the entries when creating an archive.
341376
private static string GetBasePathForCreateFromDirectory(DirectoryInfo di, bool includeBaseDirectory) =>
342377
includeBaseDirectory && di.Parent != null ? di.Parent.FullName : di.FullName;

src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectory.File.Tests.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,5 +193,65 @@ public void IncludeAllSegmentsOfPath(bool includeBaseDirectory)
193193

194194
Assert.Null(reader.GetNextEntry());
195195
}
196+
197+
[Fact]
198+
public void SkipRecursionIntoDirectorySymlinks()
199+
{
200+
using TempDirectory root = new TempDirectory();
201+
202+
string destinationArchive = Path.Join(root.Path, "destination.tar");
203+
204+
string externalDirectory = Path.Join(root.Path, "externalDirectory");
205+
Directory.CreateDirectory(externalDirectory);
206+
207+
File.Create(Path.Join(externalDirectory, "file.txt")).Dispose();
208+
209+
string sourceDirectoryName = Path.Join(root.Path, "baseDirectory");
210+
Directory.CreateDirectory(sourceDirectoryName);
211+
212+
string subDirectory = Path.Join(sourceDirectoryName, "subDirectory");
213+
Directory.CreateSymbolicLink(subDirectory, externalDirectory); // Should not recurse here
214+
215+
TarFile.CreateFromDirectory(sourceDirectoryName, destinationArchive, includeBaseDirectory: false);
216+
217+
using FileStream archiveStream = File.OpenRead(destinationArchive);
218+
using TarReader reader = new(archiveStream, leaveOpen: false);
219+
220+
TarEntry entry = reader.GetNextEntry();
221+
Assert.NotNull(entry);
222+
Assert.Equal("subDirectory/", entry.Name);
223+
Assert.Equal(TarEntryType.SymbolicLink, entry.EntryType);
224+
225+
Assert.Null(reader.GetNextEntry()); // file.txt should not be found
226+
}
227+
228+
[Fact]
229+
public void SkipRecursionIntoBaseDirectorySymlink()
230+
{
231+
using TempDirectory root = new TempDirectory();
232+
233+
string destinationArchive = Path.Join(root.Path, "destination.tar");
234+
235+
string externalDirectory = Path.Join(root.Path, "externalDirectory");
236+
Directory.CreateDirectory(externalDirectory);
237+
238+
string subDirectory = Path.Join(externalDirectory, "subDirectory");
239+
Directory.CreateDirectory(subDirectory);
240+
241+
string sourceDirectoryName = Path.Join(root.Path, "baseDirectory");
242+
Directory.CreateSymbolicLink(sourceDirectoryName, externalDirectory);
243+
244+
TarFile.CreateFromDirectory(sourceDirectoryName, destinationArchive, includeBaseDirectory: true); // Base directory is a symlink, do not recurse
245+
246+
using FileStream archiveStream = File.OpenRead(destinationArchive);
247+
using TarReader reader = new(archiveStream, leaveOpen: false);
248+
249+
TarEntry entry = reader.GetNextEntry();
250+
Assert.NotNull(entry);
251+
Assert.Equal("baseDirectory/", entry.Name);
252+
Assert.Equal(TarEntryType.SymbolicLink, entry.EntryType);
253+
254+
Assert.Null(reader.GetNextEntry());
255+
}
196256
}
197257
}

src/libraries/System.Formats.Tar/tests/TarFile/TarFile.CreateFromDirectoryAsync.File.Tests.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,5 +237,65 @@ public async Task IncludeAllSegmentsOfPath_Async(bool includeBaseDirectory)
237237
}
238238
}
239239
}
240+
241+
[Fact]
242+
public async Task SkipRecursionIntoDirectorySymlinksAsync()
243+
{
244+
using TempDirectory root = new TempDirectory();
245+
246+
string destinationArchive = Path.Join(root.Path, "destination.tar");
247+
248+
string externalDirectory = Path.Join(root.Path, "externalDirectory");
249+
Directory.CreateDirectory(externalDirectory);
250+
251+
File.Create(Path.Join(externalDirectory, "file.txt")).Dispose();
252+
253+
string sourceDirectoryName = Path.Join(root.Path, "baseDirectory");
254+
Directory.CreateDirectory(sourceDirectoryName);
255+
256+
string subDirectory = Path.Join(sourceDirectoryName, "subDirectory");
257+
Directory.CreateSymbolicLink(subDirectory, externalDirectory); // Should not recurse here
258+
259+
await TarFile.CreateFromDirectoryAsync(sourceDirectoryName, destinationArchive, includeBaseDirectory: false);
260+
261+
await using FileStream archiveStream = File.OpenRead(destinationArchive);
262+
await using TarReader reader = new(archiveStream, leaveOpen: false);
263+
264+
TarEntry entry = await reader.GetNextEntryAsync();
265+
Assert.NotNull(entry);
266+
Assert.Equal("subDirectory/", entry.Name);
267+
Assert.Equal(TarEntryType.SymbolicLink, entry.EntryType);
268+
269+
Assert.Null(await reader.GetNextEntryAsync()); // file.txt should not be found
270+
}
271+
272+
[Fact]
273+
public async Task SkipRecursionIntoBaseDirectorySymlinkAsync()
274+
{
275+
using TempDirectory root = new TempDirectory();
276+
277+
string destinationArchive = Path.Join(root.Path, "destination.tar");
278+
279+
string externalDirectory = Path.Join(root.Path, "externalDirectory");
280+
Directory.CreateDirectory(externalDirectory);
281+
282+
string subDirectory = Path.Join(externalDirectory, "subDirectory");
283+
Directory.CreateDirectory(subDirectory);
284+
285+
string sourceDirectoryName = Path.Join(root.Path, "baseDirectory");
286+
Directory.CreateSymbolicLink(sourceDirectoryName, externalDirectory);
287+
288+
await TarFile.CreateFromDirectoryAsync(sourceDirectoryName, destinationArchive, includeBaseDirectory: true); // Base directory is a symlink, do not recurse
289+
290+
await using FileStream archiveStream = File.OpenRead(destinationArchive);
291+
await using TarReader reader = new(archiveStream, leaveOpen: false);
292+
293+
TarEntry entry = await reader.GetNextEntryAsync();
294+
Assert.NotNull(entry);
295+
Assert.Equal("baseDirectory/", entry.Name);
296+
Assert.Equal(TarEntryType.SymbolicLink, entry.EntryType);
297+
298+
Assert.Null(await reader.GetNextEntryAsync()); // subDirectory should not be found
299+
}
240300
}
241301
}

0 commit comments

Comments
 (0)