Skip to content

Commit 91a75b9

Browse files
committed
Fix credential_process failing when executable path contains spaces on Windows.
1 parent edae3d6 commit 91a75b9

3 files changed

Lines changed: 148 additions & 7 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"core": {
3+
"changeLogMessages": [
4+
"Fix credential_process failing when executable path contains spaces on Windows."
5+
],
6+
"type": "patch",
7+
"updateMinimum": false
8+
}
9+
}

sdk/src/Core/Amazon.Runtime/Credentials/ProcessAWSCredentials.cs

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,19 +74,24 @@ public ProcessAWSCredentials(string processCredentialInfo) : this(processCredent
7474
public ProcessAWSCredentials(string processCredentialInfo, string accountId)
7575
{
7676
processCredentialInfo = processCredentialInfo.Trim();
77-
78-
//Default to cmd on Windows since that is the only thing BCL runs on.
79-
var fileName = "cmd.exe";
80-
var arguments = $@"/c {processCredentialInfo}";
8177
_accountId = accountId;
78+
79+
string fileName;
80+
string arguments;
81+
8282
#if NETSTANDARD
83-
//If it is netstandard and not running on Windows use sh.
84-
if(!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
83+
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
8584
{
8685
fileName = "sh";
87-
var escapedArgs = processCredentialInfo.Replace("\\", "\\\\").Replace("\"", "\\\"");
86+
var escapedArgs = processCredentialInfo.Replace("\\", "\\\\").Replace("\"", "\\\"");
8887
arguments = $"-c \"{escapedArgs}\"";
8988
}
89+
else
90+
{
91+
(fileName, arguments) = ParseCredentialProcessCommand(processCredentialInfo);
92+
}
93+
#else
94+
(fileName, arguments) = ParseCredentialProcessCommand(processCredentialInfo);
9095
#endif
9196
_processStartInfo = new ProcessStartInfo
9297
{
@@ -164,6 +169,57 @@ public async Task<CredentialsRefreshState> DetermineProcessCredentialAsync()
164169
#endregion
165170

166171
#region Private methods
172+
173+
/// <summary>
174+
/// Parses the credential_process command string into an executable path and arguments,
175+
/// correctly handling double-quoted paths that contain spaces per the AWS SDK specification
176+
/// (https://docs.aws.amazon.com/sdkref/latest/guide/feature-process-credentials.html).
177+
/// This avoids using cmd.exe, which has issues with quoted paths
178+
/// and hangs when cmd.exe is disabled via Group Policy.
179+
/// </summary>
180+
private static (string FileName, string Arguments) ParseCredentialProcessCommand(string command)
181+
{
182+
if (string.IsNullOrEmpty(command))
183+
{
184+
return (string.Empty, string.Empty);
185+
}
186+
187+
string fileName;
188+
string arguments;
189+
190+
if (command[0] == '"')
191+
{
192+
var closingQuote = command.IndexOf('"', 1);
193+
if (closingQuote == -1)
194+
{
195+
// Fail fast on malformed input, consistent with botocore's _windows_shell_split().
196+
throw new ProcessAWSCredentialException(
197+
"No closing quotation mark found in credential_process command.");
198+
}
199+
200+
fileName = command.Substring(1, closingQuote - 1);
201+
arguments = closingQuote + 1 < command.Length
202+
? command.Substring(closingQuote + 1).TrimStart()
203+
: string.Empty;
204+
}
205+
else
206+
{
207+
var spaceIndex = command.IndexOfAny(new[] { ' ', '\t' });
208+
if (spaceIndex == -1)
209+
{
210+
fileName = command;
211+
arguments = string.Empty;
212+
}
213+
else
214+
{
215+
fileName = command.Substring(0, spaceIndex);
216+
arguments = command.Substring(spaceIndex + 1).TrimStart();
217+
}
218+
}
219+
220+
return (fileName, arguments);
221+
}
222+
167223
[SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands")]
168224
private CredentialsRefreshState SetCredentialsRefreshState(ProcessExecutionResult processInfo)
169225
{

sdk/test/UnitTests/Custom/Runtime/Credentials/ProcessAWSCredentialsTest.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,5 +149,81 @@ public void ValidateRequiredFieldsCheck()
149149
}
150150
}
151151
#endif
152+
153+
[TestMethod]
154+
public void QuotedExecutablePathWithSpaces()
155+
{
156+
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
157+
{
158+
return;
159+
}
160+
161+
var dirWithSpace = Path.Combine(Path.GetTempPath(), $"aws sdk test {Guid.NewGuid():N}");
162+
Directory.CreateDirectory(dirWithSpace);
163+
164+
try
165+
{
166+
var scriptCopy = Path.Combine(dirWithSpace, "windows-credentials-script.bat");
167+
File.Copy(Executable, scriptCopy, overwrite: true);
168+
169+
var processCredential = new ProcessAWSCredentials(
170+
$"\"{scriptCopy}\" {ArgumentsBasic} {ValidVersionNumber}");
171+
var credentialsRefreshState = processCredential.DetermineProcessCredential();
172+
Assert.AreEqual(ActualAccessKey, credentialsRefreshState.Credentials.AccessKey);
173+
Assert.AreEqual(ActualSecretKey, credentialsRefreshState.Credentials.SecretKey);
174+
}
175+
finally
176+
{
177+
Directory.Delete(dirWithSpace, recursive: true);
178+
}
179+
}
180+
181+
[TestMethod]
182+
public void UnquotedExecutablePathWithoutSpaces()
183+
{
184+
var processCredential = new ProcessAWSCredentials(
185+
$"{Executable} {ArgumentsBasic} {ValidVersionNumber}");
186+
var credentialsRefreshState = processCredential.DetermineProcessCredential();
187+
Assert.AreEqual(ActualAccessKey, credentialsRefreshState.Credentials.AccessKey);
188+
Assert.AreEqual(ActualSecretKey, credentialsRefreshState.Credentials.SecretKey);
189+
}
190+
191+
192+
[TestMethod]
193+
public void QuotedExecutablePathWithMultipleArguments()
194+
{
195+
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
196+
{
197+
return;
198+
}
199+
200+
var dirWithSpace = Path.Combine(Path.GetTempPath(), $"aws sdk test {Guid.NewGuid():N}");
201+
Directory.CreateDirectory(dirWithSpace);
202+
203+
try
204+
{
205+
var scriptCopy = Path.Combine(dirWithSpace, "windows-credentials-script.bat");
206+
File.Copy(Executable, scriptCopy, overwrite: true);
207+
208+
var processCredential = new ProcessAWSCredentials(
209+
$"\"{scriptCopy}\" {ArgumentsSession} {ValidVersionNumber}");
210+
var credentialsRefreshState = processCredential.DetermineProcessCredential();
211+
Assert.AreEqual(ActualAccessKey, credentialsRefreshState.Credentials.AccessKey);
212+
Assert.AreEqual(ActualSecretKey, credentialsRefreshState.Credentials.SecretKey);
213+
Assert.IsNotNull(credentialsRefreshState.Credentials.Token);
214+
}
215+
finally
216+
{
217+
Directory.Delete(dirWithSpace, recursive: true);
218+
}
219+
}
220+
221+
[TestMethod]
222+
public void UnmatchedDoubleQuoteThrows()
223+
{
224+
Assert.ThrowsException<ProcessAWSCredentialException>(() =>
225+
new ProcessAWSCredentials("\"C:\\Program Files\\foo.exe").DetermineProcessCredential());
226+
}
227+
152228
}
153229
}

0 commit comments

Comments
 (0)