Skip to content

Commit ea48de8

Browse files
Escape Single Quotes in File Paths (#3543)
* Re-establish seperate auth paths for WAM enabled/disabled * Escape Single Quotes in File Paths * remove auth changes from this pr
1 parent ca46c32 commit ea48de8

File tree

3 files changed

+132
-2
lines changed

3 files changed

+132
-2
lines changed
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// ------------------------------------------------------------------------------
2+
// Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License. See License in the project root for license information.
3+
// ------------------------------------------------------------------------------
4+
5+
using System.Collections.Generic;
6+
using System.Linq;
7+
using System.Management.Automation.Language;
8+
using Xunit;
9+
10+
namespace Microsoft.Graph.Authentication.Test.Utilities.Runtime.Cmdlets
11+
{
12+
/// <summary>
13+
/// Tests to verify that path escaping works correctly for PowerShell commands.
14+
/// These tests validate the fix for CVE-like vulnerability where paths with single quotes
15+
/// could break PowerShell command syntax or potentially allow command injection.
16+
/// </summary>
17+
public class StringEscapingTests
18+
{
19+
public static IEnumerable<object[]> PathsWithSingleQuotes =>
20+
new List<object[]>
21+
{
22+
new object[] { "C:\\User's Documents\\Module.psd1", "C:\\User''s Documents\\Module.psd1" },
23+
new object[] { "C:\\Test's\\Path\\File.ps1", "C:\\Test''s\\Path\\File.ps1" },
24+
new object[] { "C:\\Users\\John's Folder\\Scripts", "C:\\Users\\John''s Folder\\Scripts" },
25+
new object[] { "C:\\It's\\Working\\Test.psm1", "C:\\It''s\\Working\\Test.psm1" },
26+
new object[] { "C:\\Multiple'Single'Quotes\\File.ps1", "C:\\Multiple''Single''Quotes\\File.ps1" }
27+
};
28+
29+
public static IEnumerable<object[]> PathsWithoutSingleQuotes =>
30+
new List<object[]>
31+
{
32+
new object[] { "C:\\Users\\Documents\\Module.psd1" },
33+
new object[] { "C:\\Windows\\System32\\Test.ps1" },
34+
new object[] { "C:\\Program Files\\Application\\Script.psm1" }
35+
};
36+
37+
public static IEnumerable<object[]> MaliciousPaths =>
38+
new List<object[]>
39+
{
40+
// Path that attempts command injection
41+
new object[] { "C:\\Test'; Write-Output 'INJECTED'; '\\Module.psd1", "C:\\Test''; Write-Output ''INJECTED''; ''\\Module.psd1" },
42+
// Path that attempts to close the string and run additional command
43+
new object[] { "C:\\Malicious' -and $true -eq $true #\\Test.ps1", "C:\\Malicious'' -and $true -eq $true #\\Test.ps1" }
44+
};
45+
46+
[Theory]
47+
[MemberData(nameof(PathsWithSingleQuotes))]
48+
public void EscapeSingleQuotedStringContent_WithSingleQuote_ShouldDoubleTheQuote(string input, string expected)
49+
{
50+
// Act
51+
var result = CodeGeneration.EscapeSingleQuotedStringContent(input);
52+
53+
// Assert
54+
Assert.Equal(expected, result);
55+
}
56+
57+
[Theory]
58+
[MemberData(nameof(PathsWithoutSingleQuotes))]
59+
public void EscapeSingleQuotedStringContent_WithoutSingleQuote_ShouldReturnUnchanged(string input)
60+
{
61+
// Act
62+
var result = CodeGeneration.EscapeSingleQuotedStringContent(input);
63+
64+
// Assert
65+
Assert.Equal(input, result);
66+
}
67+
68+
[Theory]
69+
[MemberData(nameof(MaliciousPaths))]
70+
public void EscapeSingleQuotedStringContent_WithMaliciousInput_ShouldEscapeAllQuotes(string input, string expected)
71+
{
72+
// Act
73+
var result = CodeGeneration.EscapeSingleQuotedStringContent(input);
74+
75+
// Assert
76+
Assert.Equal(expected, result);
77+
}
78+
79+
[Fact]
80+
public void EscapeSingleQuotedStringContent_WithEmptyString_ShouldReturnEmpty()
81+
{
82+
// Arrange
83+
var input = string.Empty;
84+
85+
// Act
86+
var result = CodeGeneration.EscapeSingleQuotedStringContent(input);
87+
88+
// Assert
89+
Assert.Equal(string.Empty, result);
90+
}
91+
92+
[Fact]
93+
public void EscapedPath_WhenUsedInPowerShellCommand_ShouldNotBreakSyntax()
94+
{
95+
// Arrange
96+
var pathWithQuote = "C:\\User's Documents\\Module.psd1";
97+
var escapedPath = CodeGeneration.EscapeSingleQuotedStringContent(pathWithQuote);
98+
99+
// Act - Simulate the command construction like in GetModuleCmdlet
100+
var command = $"(Get-Command -Module (Import-Module '{escapedPath}' -PassThru))";
101+
102+
// Assert - Verify the command has properly escaped quotes
103+
Assert.Contains("User''s Documents", command);
104+
Assert.DoesNotContain("User's Documents", command);
105+
106+
// Verify opening and closing quotes match
107+
var singleQuoteCount = command.Count(c => c == '\'');
108+
Assert.Equal(4, singleQuoteCount); // 2 pairs of quotes around the path
109+
}
110+
111+
[Fact]
112+
public void EscapedScriptFolder_WhenUsedInPowerShellCommand_ShouldNotBreakSyntax()
113+
{
114+
// Arrange
115+
var folderWithQuote = "C:\\User's Scripts";
116+
var escapedFolder = CodeGeneration.EscapeSingleQuotedStringContent(folderWithQuote);
117+
118+
// Act - Simulate the command construction like in GetScriptCmdlet
119+
var command = $"Get-ChildItem -Path '{escapedFolder}' -Recurse -Include '*.ps1' -File";
120+
121+
// Assert - Verify the command has properly escaped quotes
122+
Assert.Contains("User''s Scripts", command);
123+
Assert.DoesNotContain("User's Scripts", command);
124+
}
125+
}
126+
}

src/Authentication/Authentication/Utilities/Runtime/Cmdlets/GetModuleCmdlet.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Collections.Generic;
77
using System.Linq;
88
using System.Management.Automation;
9+
using System.Management.Automation.Language;
910

1011
namespace Microsoft.Graph.PowerShell.Authentication.Utilities.Runtime.Cmdlets
1112
{
@@ -70,7 +71,8 @@ protected override void ProcessRecord()
7071

7172
private IEnumerable<CommandInfo> GetModuleCmdlets(string modulePath)
7273
{
73-
var getCmdletsCommand = $"(Get-Command -Module (Import-Module '{modulePath}' -PassThru))";
74+
var escapedModulePath = CodeGeneration.EscapeSingleQuotedStringContent(modulePath);
75+
var getCmdletsCommand = $"(Get-Command -Module (Import-Module '{escapedModulePath}' -PassThru))";
7476
return PSCmdletExtensions.RunScript<CommandInfo>(getCmdletsCommand);
7577
}
7678
}

src/Authentication/Authentication/Utilities/Runtime/Cmdlets/GetScriptCmdlet.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Collections.Generic;
66
using System.Linq;
77
using System.Management.Automation;
8+
using System.Management.Automation.Language;
89

910
namespace Microsoft.Graph.PowerShell.Authentication.Utilities.Runtime.Cmdlets
1011
{
@@ -67,9 +68,10 @@ protected override void ProcessRecord()
6768
private IEnumerable<FunctionInfo> GetScriptCmdlets(string scriptFolder)
6869
{
6970
// https://stackoverflow.com/a/40969712/294804
71+
var escapedScriptFolder = CodeGeneration.EscapeSingleQuotedStringContent(scriptFolder);
7072
var getCmdletsCommand = $@"
7173
$currentFunctions = Get-ChildItem function:
72-
Get-ChildItem -Path '{scriptFolder}' -Recurse -Include '*.ps1' -File | ForEach-Object {{ . $_.FullName }}
74+
Get-ChildItem -Path '{escapedScriptFolder}' -Recurse -Include '*.ps1' -File | ForEach-Object {{ . $_.FullName }}
7375
Get-ChildItem function: | Where-Object {{ ($currentFunctions -notcontains $_) -and $_.CmdletBinding }}
7476
";
7577
return this.RunScript<FunctionInfo>(getCmdletsCommand);

0 commit comments

Comments
 (0)