Apply more sanity checks when handling files from archives

This commit is contained in:
Bartłomiej Dach
2026-03-02 12:07:11 +01:00
parent 033e13cb3b
commit 9c8dfaf386
3 changed files with 55 additions and 1 deletions

View File

@@ -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)

View File

@@ -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));
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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
{
/// <summary>
/// Returns whether <paramref name="path"/> is potentially susceptible to path traversal style attacks.
/// </summary>
public static bool IncursPathTraversalRisk(string path)
=> path.Contains("../", StringComparison.Ordinal) || path.Contains("..\\", StringComparison.Ordinal) || Path.IsPathRooted(path);
/// <summary>
/// Returns whether <paramref name="child"/> is a subdirectory (direct or nested) of <paramref name="parent"/>.
/// </summary>
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;
}
}
}