diff --git a/src/Authentication/Authentication.Test/Utilities/Runtime/Cmdlets/StringEscapingTests.cs b/src/Authentication/Authentication.Test/Utilities/Runtime/Cmdlets/StringEscapingTests.cs
new file mode 100644
index 00000000000..e75584d4c93
--- /dev/null
+++ b/src/Authentication/Authentication.Test/Utilities/Runtime/Cmdlets/StringEscapingTests.cs
@@ -0,0 +1,126 @@
+// ------------------------------------------------------------------------------
+// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
+// ------------------------------------------------------------------------------
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Management.Automation.Language;
+using Xunit;
+
+namespace Microsoft.Graph.Authentication.Test.Utilities.Runtime.Cmdlets
+{
+ ///
+ /// Tests to verify that path escaping works correctly for PowerShell commands.
+ /// These tests validate the fix for CVE-like vulnerability where paths with single quotes
+ /// could break PowerShell command syntax or potentially allow command injection.
+ ///
+ public class StringEscapingTests
+ {
+ public static IEnumerable PathsWithSingleQuotes =>
+ new List
+ {
+ new object[] { "C:\\User's Documents\\Module.psd1", "C:\\User''s Documents\\Module.psd1" },
+ new object[] { "C:\\Test's\\Path\\File.ps1", "C:\\Test''s\\Path\\File.ps1" },
+ new object[] { "C:\\Users\\John's Folder\\Scripts", "C:\\Users\\John''s Folder\\Scripts" },
+ new object[] { "C:\\It's\\Working\\Test.psm1", "C:\\It''s\\Working\\Test.psm1" },
+ new object[] { "C:\\Multiple'Single'Quotes\\File.ps1", "C:\\Multiple''Single''Quotes\\File.ps1" }
+ };
+
+ public static IEnumerable PathsWithoutSingleQuotes =>
+ new List
+ {
+ new object[] { "C:\\Users\\Documents\\Module.psd1" },
+ new object[] { "C:\\Windows\\System32\\Test.ps1" },
+ new object[] { "C:\\Program Files\\Application\\Script.psm1" }
+ };
+
+ public static IEnumerable MaliciousPaths =>
+ new List
+ {
+ // Path that attempts command injection
+ new object[] { "C:\\Test'; Write-Output 'INJECTED'; '\\Module.psd1", "C:\\Test''; Write-Output ''INJECTED''; ''\\Module.psd1" },
+ // Path that attempts to close the string and run additional command
+ new object[] { "C:\\Malicious' -and $true -eq $true #\\Test.ps1", "C:\\Malicious'' -and $true -eq $true #\\Test.ps1" }
+ };
+
+ [Theory]
+ [MemberData(nameof(PathsWithSingleQuotes))]
+ public void EscapeSingleQuotedStringContent_WithSingleQuote_ShouldDoubleTheQuote(string input, string expected)
+ {
+ // Act
+ var result = CodeGeneration.EscapeSingleQuotedStringContent(input);
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ [Theory]
+ [MemberData(nameof(PathsWithoutSingleQuotes))]
+ public void EscapeSingleQuotedStringContent_WithoutSingleQuote_ShouldReturnUnchanged(string input)
+ {
+ // Act
+ var result = CodeGeneration.EscapeSingleQuotedStringContent(input);
+
+ // Assert
+ Assert.Equal(input, result);
+ }
+
+ [Theory]
+ [MemberData(nameof(MaliciousPaths))]
+ public void EscapeSingleQuotedStringContent_WithMaliciousInput_ShouldEscapeAllQuotes(string input, string expected)
+ {
+ // Act
+ var result = CodeGeneration.EscapeSingleQuotedStringContent(input);
+
+ // Assert
+ Assert.Equal(expected, result);
+ }
+
+ [Fact]
+ public void EscapeSingleQuotedStringContent_WithEmptyString_ShouldReturnEmpty()
+ {
+ // Arrange
+ var input = string.Empty;
+
+ // Act
+ var result = CodeGeneration.EscapeSingleQuotedStringContent(input);
+
+ // Assert
+ Assert.Equal(string.Empty, result);
+ }
+
+ [Fact]
+ public void EscapedPath_WhenUsedInPowerShellCommand_ShouldNotBreakSyntax()
+ {
+ // Arrange
+ var pathWithQuote = "C:\\User's Documents\\Module.psd1";
+ var escapedPath = CodeGeneration.EscapeSingleQuotedStringContent(pathWithQuote);
+
+ // Act - Simulate the command construction like in GetModuleCmdlet
+ var command = $"(Get-Command -Module (Import-Module '{escapedPath}' -PassThru))";
+
+ // Assert - Verify the command has properly escaped quotes
+ Assert.Contains("User''s Documents", command);
+ Assert.DoesNotContain("User's Documents", command);
+
+ // Verify opening and closing quotes match
+ var singleQuoteCount = command.Count(c => c == '\'');
+ Assert.Equal(4, singleQuoteCount); // 2 pairs of quotes around the path
+ }
+
+ [Fact]
+ public void EscapedScriptFolder_WhenUsedInPowerShellCommand_ShouldNotBreakSyntax()
+ {
+ // Arrange
+ var folderWithQuote = "C:\\User's Scripts";
+ var escapedFolder = CodeGeneration.EscapeSingleQuotedStringContent(folderWithQuote);
+
+ // Act - Simulate the command construction like in GetScriptCmdlet
+ var command = $"Get-ChildItem -Path '{escapedFolder}' -Recurse -Include '*.ps1' -File";
+
+ // Assert - Verify the command has properly escaped quotes
+ Assert.Contains("User''s Scripts", command);
+ Assert.DoesNotContain("User's Scripts", command);
+ }
+ }
+}
diff --git a/src/Authentication/Authentication/Utilities/Runtime/Cmdlets/GetModuleCmdlet.cs b/src/Authentication/Authentication/Utilities/Runtime/Cmdlets/GetModuleCmdlet.cs
index 520fb2a6afe..1f78b8ee49d 100644
--- a/src/Authentication/Authentication/Utilities/Runtime/Cmdlets/GetModuleCmdlet.cs
+++ b/src/Authentication/Authentication/Utilities/Runtime/Cmdlets/GetModuleCmdlet.cs
@@ -6,6 +6,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation;
+using System.Management.Automation.Language;
namespace Microsoft.Graph.PowerShell.Authentication.Utilities.Runtime.Cmdlets
{
@@ -70,7 +71,8 @@ protected override void ProcessRecord()
private IEnumerable GetModuleCmdlets(string modulePath)
{
- var getCmdletsCommand = $"(Get-Command -Module (Import-Module '{modulePath}' -PassThru))";
+ var escapedModulePath = CodeGeneration.EscapeSingleQuotedStringContent(modulePath);
+ var getCmdletsCommand = $"(Get-Command -Module (Import-Module '{escapedModulePath}' -PassThru))";
return PSCmdletExtensions.RunScript(getCmdletsCommand);
}
}
diff --git a/src/Authentication/Authentication/Utilities/Runtime/Cmdlets/GetScriptCmdlet.cs b/src/Authentication/Authentication/Utilities/Runtime/Cmdlets/GetScriptCmdlet.cs
index 6c3053d3d5b..737188325e8 100644
--- a/src/Authentication/Authentication/Utilities/Runtime/Cmdlets/GetScriptCmdlet.cs
+++ b/src/Authentication/Authentication/Utilities/Runtime/Cmdlets/GetScriptCmdlet.cs
@@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation;
+using System.Management.Automation.Language;
namespace Microsoft.Graph.PowerShell.Authentication.Utilities.Runtime.Cmdlets
{
@@ -67,9 +68,10 @@ protected override void ProcessRecord()
private IEnumerable GetScriptCmdlets(string scriptFolder)
{
// https://stackoverflow.com/a/40969712/294804
+ var escapedScriptFolder = CodeGeneration.EscapeSingleQuotedStringContent(scriptFolder);
var getCmdletsCommand = $@"
$currentFunctions = Get-ChildItem function:
- Get-ChildItem -Path '{scriptFolder}' -Recurse -Include '*.ps1' -File | ForEach-Object {{ . $_.FullName }}
+ Get-ChildItem -Path '{escapedScriptFolder}' -Recurse -Include '*.ps1' -File | ForEach-Object {{ . $_.FullName }}
Get-ChildItem function: | Where-Object {{ ($currentFunctions -notcontains $_) -and $_.CmdletBinding }}
";
return this.RunScript(getCmdletsCommand);