From 70ae53204c344c1b74bad19b718de87ccee68784 Mon Sep 17 00:00:00 2001 From: ramsessanchez <63934382+ramsessanchez@users.noreply.github.com> Date: Tue, 24 Feb 2026 11:58:58 -0800 Subject: [PATCH 1/3] Re-establish seperate auth paths for WAM enabled/disabled --- .../Utilities/AuthenticationHelpers.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Authentication/Authentication.Core/Utilities/AuthenticationHelpers.cs b/src/Authentication/Authentication.Core/Utilities/AuthenticationHelpers.cs index 8080206dd3..c781788e7e 100644 --- a/src/Authentication/Authentication.Core/Utilities/AuthenticationHelpers.cs +++ b/src/Authentication/Authentication.Core/Utilities/AuthenticationHelpers.cs @@ -127,11 +127,18 @@ private static async Task GetInteractiveBrowserCre if (ShouldUseWam(authContext)) { GraphSession.Instance.OutputWriter.WriteWarning("Note: Sign in by Web Account Manager (WAM) is enabled by default on Windows. If using an embedded terminal, the interactive browser window may be hidden behind other windows."); + authRecord = await Task.Run(() => + { + return interactiveBrowserCredential.Authenticate(new TokenRequestContext(authContext.Scopes), cancellationToken); + }); } - authRecord = await Task.Run(() => + else { - return interactiveBrowserCredential.Authenticate(new TokenRequestContext(authContext.Scopes), cancellationToken); - }); + authRecord = await Task.Run(() => + { + return interactiveBrowserCredential.AuthenticateAsync(new TokenRequestContext(authContext.Scopes), cancellationToken); + }); + } await WriteAuthRecordAsync(authRecord).ConfigureAwait(false); return interactiveBrowserCredential; } From 5ab29b8e1a948a2e697b5125deaf9b42aa5c242a Mon Sep 17 00:00:00 2001 From: ramsessanchez <63934382+ramsessanchez@users.noreply.github.com> Date: Tue, 24 Feb 2026 12:37:59 -0800 Subject: [PATCH 2/3] Escape Single Quotes in File Paths --- .../Runtime/Cmdlets/StringEscapingTests.cs | 126 ++++++++++++++++++ .../Runtime/Cmdlets/GetModuleCmdlet.cs | 4 +- .../Runtime/Cmdlets/GetScriptCmdlet.cs | 4 +- 3 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 src/Authentication/Authentication.Test/Utilities/Runtime/Cmdlets/StringEscapingTests.cs 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 0000000000..e75584d4c9 --- /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 520fb2a6af..1f78b8ee49 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 6c3053d3d5..737188325e 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); From a5edc55fcb2b753784158db3cf13c85129ecad5a Mon Sep 17 00:00:00 2001 From: ramsessanchez <63934382+ramsessanchez@users.noreply.github.com> Date: Tue, 24 Feb 2026 13:17:25 -0800 Subject: [PATCH 3/3] remove auth changes from this pr --- .../Utilities/AuthenticationHelpers.cs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/Authentication/Authentication.Core/Utilities/AuthenticationHelpers.cs b/src/Authentication/Authentication.Core/Utilities/AuthenticationHelpers.cs index c781788e7e..8080206dd3 100644 --- a/src/Authentication/Authentication.Core/Utilities/AuthenticationHelpers.cs +++ b/src/Authentication/Authentication.Core/Utilities/AuthenticationHelpers.cs @@ -127,18 +127,11 @@ private static async Task GetInteractiveBrowserCre if (ShouldUseWam(authContext)) { GraphSession.Instance.OutputWriter.WriteWarning("Note: Sign in by Web Account Manager (WAM) is enabled by default on Windows. If using an embedded terminal, the interactive browser window may be hidden behind other windows."); - authRecord = await Task.Run(() => - { - return interactiveBrowserCredential.Authenticate(new TokenRequestContext(authContext.Scopes), cancellationToken); - }); } - else + authRecord = await Task.Run(() => { - authRecord = await Task.Run(() => - { - return interactiveBrowserCredential.AuthenticateAsync(new TokenRequestContext(authContext.Scopes), cancellationToken); - }); - } + return interactiveBrowserCredential.Authenticate(new TokenRequestContext(authContext.Scopes), cancellationToken); + }); await WriteAuthRecordAsync(authRecord).ConfigureAwait(false); return interactiveBrowserCredential; }