diff --git a/osu.Game/Database/ModelManager.cs b/osu.Game/Database/ModelManager.cs index e96a8cc1b1..2eaba596d6 100644 --- a/osu.Game/Database/ModelManager.cs +++ b/osu.Game/Database/ModelManager.cs @@ -12,6 +12,7 @@ using osu.Game.Beatmaps; using osu.Game.Extensions; using osu.Game.Models; using osu.Game.Overlays.Notifications; +using osu.Game.Utils; using Realms; namespace osu.Game.Database @@ -87,6 +88,10 @@ namespace osu.Game.Database public void AddFile(TModel item, Stream contents, string filename, Realm realm) { filename = filename.ToStandardisedPath(); + + if (FilesystemSanityCheckHelpers.IncursPathTraversalRisk(filename)) + throw new InvalidOperationException($@"Filename ""{filename}"" is not allowed."); + var existing = item.GetFile(filename); if (existing != null) diff --git a/osu.Game/Database/RealmArchiveModelImporter.cs b/osu.Game/Database/RealmArchiveModelImporter.cs index af57d5ec5b..1836e4ba80 100644 --- a/osu.Game/Database/RealmArchiveModelImporter.cs +++ b/osu.Game/Database/RealmArchiveModelImporter.cs @@ -17,6 +17,7 @@ using osu.Game.Extensions; using osu.Game.IO.Archives; using osu.Game.Models; using osu.Game.Overlays.Notifications; +using osu.Game.Utils; using Realms; namespace osu.Game.Database @@ -221,7 +222,15 @@ namespace osu.Game.Database foreach (string piece in realmFile.Filename.Split('/').Select(f => f.GetValidFilename())) destinationPath = Path.Combine(destinationPath, piece); - Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!); + string destinationDirectory = Path.GetDirectoryName(destinationPath)!; + + if (!FilesystemSanityCheckHelpers.IsSubDirectory(parent: mountedPath, child: destinationDirectory)) + { + Logger.Log($@"Skipping attempt to mount {realmFile.Filename} due to detected escape out of mounted path.", LoggingTarget.Database); + continue; + } + + Directory.CreateDirectory(destinationDirectory); // Consider using hard links here to make this instant. using (var inStream = Files.Storage.GetStream(sourcePath)) @@ -361,6 +370,9 @@ namespace osu.Game.Database // We intentionally delay adding to realm to avoid blocking on a write during disk operations. foreach (var filenames in getShortenedFilenames(archive)) { + if (FilesystemSanityCheckHelpers.IncursPathTraversalRisk(filenames.shortened)) + throw new InvalidOperationException($@"Filename ""{filenames.original}"" is not allowed."); + using (Stream s = archive.GetStream(filenames.original)) files.Add(new RealmNamedFileUsage(Files.Add(s, realm, false, parameters.PreferHardLinks), filenames.shortened)); } diff --git a/osu.Game/Utils/FilesystemSanityCheckHelpers.cs b/osu.Game/Utils/FilesystemSanityCheckHelpers.cs new file mode 100644 index 0000000000..d734750810 --- /dev/null +++ b/osu.Game/Utils/FilesystemSanityCheckHelpers.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.IO; + +namespace osu.Game.Utils +{ + public static class FilesystemSanityCheckHelpers + { + /// + /// Returns whether is potentially susceptible to path traversal style attacks. + /// + public static bool IncursPathTraversalRisk(string path) + => path.Contains("../", StringComparison.Ordinal) || path.Contains("..\\", StringComparison.Ordinal) || Path.IsPathRooted(path); + + /// + /// Returns whether is a subdirectory (direct or nested) of . + /// + public static bool IsSubDirectory(string parent, string child) + { + // `Path.GetFullPath()` invocations are required to fully resolve the paths to unambiguous downwards-traversal-only paths. + var parentInfo = new DirectoryInfo(Path.GetFullPath(parent)); + var childInfo = new DirectoryInfo(Path.GetFullPath(child)); + + while (childInfo != null) + { + if (parentInfo.FullName == childInfo.FullName) + return true; + + childInfo = childInfo.Parent; + } + + return false; + } + } +}