using System; using System.IO; namespace Jellyfin.Extensions; /// /// Helpers for safely composing filesystem paths from untrusted input. /// /// /// has two issues that matter in /// any code that joins a trusted directory with an externally-supplied name: /// it neither normalises .. nor rejects a rooted second argument /// (a rooted second arg silently discards the first). Use the helpers below /// any time the name comes from media metadata, request input, archive /// entries, or any other channel that can be influenced by a third party. /// public static class PathHelper { /// /// Reduces a possibly-untrusted file name to a safe leaf-only name with no /// directory components. /// /// The candidate file name. /// /// The leaf component of , or null if /// the input has no usable leaf (empty, ., or ..). /// public static string? GetSafeLeafFileName(string? fileName) { if (string.IsNullOrEmpty(fileName)) { return null; } var leaf = Path.GetFileName(fileName); if (string.IsNullOrEmpty(leaf) || string.Equals(leaf, ".", StringComparison.Ordinal) || string.Equals(leaf, "..", StringComparison.Ordinal)) { return null; } return leaf; } /// /// Returns whether resolves to a path that /// equals or is contained inside . /// /// The directory the candidate must remain inside. /// The candidate absolute or relative path. /// true if the candidate is inside or equal to root; otherwise false. /// /// Both arguments are resolved via /// so .. segments are collapsed before the comparison. The root is /// compared with a trailing directory separator to prevent prefix /// collisions (e.g. /var/data must not be accepted as a parent of /// /var/dataset). /// public static bool IsContainedIn(string root, string candidate) { ArgumentException.ThrowIfNullOrEmpty(root); ArgumentException.ThrowIfNullOrEmpty(candidate); var fullRoot = Path.GetFullPath(root); var fullCandidate = Path.GetFullPath(candidate); if (string.Equals(fullCandidate, fullRoot, StringComparison.Ordinal)) { return true; } var rootWithSep = fullRoot.EndsWith(Path.DirectorySeparatorChar) ? fullRoot : fullRoot + Path.DirectorySeparatorChar; return fullCandidate.StartsWith(rootWithSep, StringComparison.Ordinal); } }