diff --git a/src/Tools/GetDocumentInsider/src/Commands/GetDocumentCommandWorker.cs b/src/Tools/GetDocumentInsider/src/Commands/GetDocumentCommandWorker.cs index 0bd4ed9be2dd..eb12baeed32b 100644 --- a/src/Tools/GetDocumentInsider/src/Commands/GetDocumentCommandWorker.cs +++ b/src/Tools/GetDocumentInsider/src/Commands/GetDocumentCommandWorker.cs @@ -29,7 +29,13 @@ internal sealed class GetDocumentCommandWorker private const string InvalidFilenameString = ".."; private const string JsonExtension = ".json"; private const string UnderscoreString = "_"; - private static readonly char[] _invalidFilenameCharacters = Path.GetInvalidFileNameChars(); + // Note: Path.GetInvalidFileNameChars() is OS-specific. We also explicitly treat directory separators as invalid + // so document names can't be interpreted as subpaths on any platform. + private static readonly char[] _invalidFilenameCharacters = Path.GetInvalidFileNameChars() + // Treat both common separators as invalid regardless of OS to avoid platform-specific behavior. + .Concat([Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar, '\\']) + .Distinct() + .ToArray(); private static readonly Encoding _utf8EncodingWithoutBOM = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); @@ -415,7 +421,8 @@ private static string GetDocumentPath(string documentName, string fileName, stri sanitizedDocumentName = sanitizedDocumentName.Replace(InvalidFilenameString, DotString); } - path = $"{fileName}_{documentName}{JsonExtension}"; + // Use the sanitized name to ensure the output path is a file name, not a user-controlled path. + path = $"{fileName}_{sanitizedDocumentName}{JsonExtension}"; } if (!string.IsNullOrEmpty(outputDirectory)) diff --git a/src/Tools/GetDocumentInsider/tests/GetDocumentTests.cs b/src/Tools/GetDocumentInsider/tests/GetDocumentTests.cs index 712ef8bc1f8c..3b9b9a2e94c0 100644 --- a/src/Tools/GetDocumentInsider/tests/GetDocumentTests.cs +++ b/src/Tools/GetDocumentInsider/tests/GetDocumentTests.cs @@ -237,4 +237,21 @@ public void GetDocument_WithEmptyFileName_Works() Assert.True(File.Exists(Path.Combine(outputPath.FullName, "Sample.json"))); Assert.True(File.Exists(Path.Combine(outputPath.FullName, "Sample_internal.json"))); } + + [Theory] + [InlineData("a/../b", "Sample_a_._b.json")] + [InlineData(@"a\..\b", "Sample_a_._b.json")] + [InlineData("..", "Sample_..json")] + public void GetDocumentPath_SanitizesDocumentName(string documentName, string expectedFileName) + { + // We validate the path generation logic directly to avoid coupling this test to sample app document names. + var method = typeof(GetDocumentCommandWorker).GetMethod("GetDocumentPath", BindingFlags.NonPublic | BindingFlags.Static); + Assert.NotNull(method); + + var outputDir = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()); + var resultPath = (string)method.Invoke(null, [documentName, "Sample", outputDir]); + + Assert.Equal(expectedFileName, Path.GetFileName(resultPath)); + Assert.Equal(outputDir, Path.GetDirectoryName(resultPath)); + } }