Skip to content

Commit 163d623

Browse files
stefong99Copilot
andauthored
Fix for Server Challenge Token Security Incident (#28177)
Co-authored-by: Copilot <[email protected]>
1 parent 3b990ea commit 163d623

File tree

4 files changed

+71
-31
lines changed

4 files changed

+71
-31
lines changed

src/StorageSync/StorageSync/ChangeLog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
- Additional information about change #1
1919
-->
2020
## Upcoming Release
21+
* Fixed security bug in token acquisition for MI server registration
2122

2223
## Version 2.5.0
2324
* Fixed the bug in server registration

src/StorageSync/StorageSync/Interop/ManagedIdentity/ServerManagedIdentityUtils.cs

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -272,9 +272,9 @@ public static async Task<string> GetChallengeToken(string requestUri, HttpClient
272272
null);
273273
}
274274

275-
var headerValue = authenticateHeaderValues.FirstOrDefault();
275+
var wwwHeader = authenticateHeaderValues.FirstOrDefault();
276276

277-
if (string.IsNullOrEmpty(headerValue) || !headerValue.Contains('='))
277+
if (string.IsNullOrEmpty(wwwHeader) || !wwwHeader.Contains('='))
278278
{
279279
throw new ServerManagedIdentityTokenException(
280280
ManagedIdentityErrorCodes.ServerManagedIdentityTokenChallengeFailed,
@@ -283,31 +283,17 @@ public static async Task<string> GetChallengeToken(string requestUri, HttpClient
283283
}
284284

285285
// Value in the header is: "Basic realm=<secret file path>"
286-
var secretFilePath = headerValue.Split('=')[1];
286+
var secretFilePath = wwwHeader.Split(new char[] { '=' }, StringSplitOptions.RemoveEmptyEntries).LastOrDefault();
287287

288-
var expectedSecretFileLocation = Environment.GetEnvironmentVariable("ProgramData") + HybridSecretFileDirectory;
289-
290-
// Validate the secret file path received is from the expected predefined directory and is of expected .key file extension.
291-
// This ensures we are not redirected by some malicious process listening on localhost:40342 into a bad secret file.
292-
if (string.IsNullOrEmpty(secretFilePath) ||
293-
!secretFilePath.Contains(expectedSecretFileLocation) ||
294-
!secretFilePath.Contains(".key"))
295-
{
296-
throw new ServerManagedIdentityTokenException(
297-
ManagedIdentityErrorCodes.ServerManagedIdentityTokenChallengeFailed,
298-
StorageSyncResources.AgentMI_InvalidSecretFileError,
299-
null);
300-
}
301-
302-
if (File.Exists(secretFilePath))
288+
if (IsSecretFilePathValid(secretFilePath))
303289
{
304290
challengeToken = File.ReadAllText(secretFilePath);
305291
}
306292
else
307293
{
308294
throw new ServerManagedIdentityTokenException(
309295
ManagedIdentityErrorCodes.ServerManagedIdentityTokenChallengeFailed,
310-
StorageSyncResources.AgentMI_MissingSecretFilePathOnServerError,
296+
StorageSyncResources.AgentMI_InvalidSecretFileError,
311297
null);
312298
}
313299
}
@@ -349,6 +335,59 @@ public static async Task<string> GetChallengeToken(string requestUri, HttpClient
349335
return challengeToken;
350336
}
351337

338+
/// <summary>
339+
/// Validate the secret file path received is from the expected predefined directory and is of expected .key file extension.
340+
/// This ensures we are not redirected by some malicious process listening on localhost:40342 into a bad secret file.
341+
/// </summary>
342+
/// <param name="secretFilePath"></param>
343+
/// <returns></returns>
344+
private static bool IsSecretFilePathValid(string secretFilePath)
345+
{
346+
// Check if the secret file path is null or empty
347+
if (string.IsNullOrEmpty(secretFilePath))
348+
{
349+
return false;
350+
}
351+
352+
// Normalize the path to prevent path traversal attacks
353+
string normalizedTokenLocation = Path.GetFullPath(secretFilePath);
354+
355+
string allowedFolder;
356+
357+
// Expected form: %ProgramData%\AzureConnectedMachineAgent\Tokens\<guid>.key
358+
var programData = Environment.GetEnvironmentVariable("ProgramData");
359+
360+
if (string.IsNullOrEmpty(programData))
361+
{
362+
// If ProgramData is not found, try to manually construct it using SystemDrive
363+
var systemDrive = Environment.GetEnvironmentVariable("SystemDrive");
364+
365+
if (string.IsNullOrEmpty(systemDrive))
366+
{
367+
throw new ServerManagedIdentityTokenException(
368+
ManagedIdentityErrorCodes.ServerManagedIdentityTokenChallengeFailed,
369+
StorageSyncResources.AgentMI_ProgramDataNotFoundError,
370+
null);
371+
}
372+
else
373+
{
374+
programData = Path.Combine(systemDrive, "ProgramData");
375+
}
376+
}
377+
378+
allowedFolder = Path.GetFullPath(Path.Combine(programData, "AzureConnectedMachineAgent", "Tokens"));
379+
380+
// Ensure the secret file is within the allowed tokens folder, exists, and ends with .key
381+
if (!normalizedTokenLocation.StartsWith(allowedFolder + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase) ||
382+
!File.Exists(normalizedTokenLocation) ||
383+
!Path.GetFileName(normalizedTokenLocation).EndsWith(".key", StringComparison.OrdinalIgnoreCase))
384+
{
385+
return false;
386+
}
387+
388+
return true;
389+
}
390+
352391
/// <summary>
353392
/// Gets the Server Type from the the StorageSync registry path. Default to <see cref="LocalServerType.HybridServer"/>
354393
/// Not using ServerManagedIdentityProvider.GetServerType because it does not necessarily do a direct registry key read.

src/StorageSync/StorageSync/Properties/StorageSyncResources.Designer.cs

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/StorageSync/StorageSync/Properties/StorageSyncResources.resx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -294,9 +294,6 @@
294294
<data name="AgentMI_InvalidSecretFileError" xml:space="preserve">
295295
<value>Secret file is invalid. It is either empty, not in the expected directory, or not of file type .key.</value>
296296
</data>
297-
<data name="AgentMI_MissingSecretFilePathOnServerError" xml:space="preserve">
298-
<value>Secret file path does not exist on the server.</value>
299-
</data>
300297
<data name="AgentMI_MissingSystemAssignedIdentityError" xml:space="preserve">
301298
<value>No system-assigned Managed Identity was found for this resource.</value>
302299
</data>
@@ -327,4 +324,7 @@
327324
<data name="AgentMI_ArcServerNotEnabled" xml:space="preserve">
328325
<value>&gt;This hybrid server is not Azure Arc-enabled. Please ensure that the Azure Connected Machine agent is installed and the server is connected to Azure Arc.</value>
329326
</data>
327+
<data name="AgentMI_ProgramDataNotFoundError" xml:space="preserve">
328+
<value>GetEnvironmentVariable failed to find 'ProgramData'</value>
329+
</data>
330330
</root>

0 commit comments

Comments
 (0)