Skip to content

Deactivate Base64 Encoded Vso Commands AB#2008236 #5158

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
May 2, 2025
Merged
36 changes: 36 additions & 0 deletions src/Agent.Sdk/Util/StringUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ public static bool AreHashesEqual(string leftValue, string rightValue)

/// <summary>
/// Finds all vso commands in the line and deactivates them
/// Also, assuming line to be base64 encoded, finds all vso commands in the decoded string and deactivates them and re-encode
/// </summary>
/// <returns>String without vso commands that can be executed</returns>
public static string DeactivateVsoCommands(string input)
Expand All @@ -291,6 +292,41 @@ public static string DeactivateVsoCommands(string input)
return string.Empty;
}

try
{
input = DeactivateVsoCommandsIfBase64Encoded(input);
}
catch (FormatException)
{
// Ignore exception and continue to deactivate vso commands in the input string.
}
return ScrapVsoCommands(input);
}

/// <summary>
/// Tries to decode the input string assuming it to be base64 encoded and
/// scraps vso command if any and re-encodes the updated string.
/// An exception is thrown for the case when the input is not base64 encoded.
/// </summary>
/// <returns>String without vso commands that can be executed</returns>
public static string DeactivateVsoCommandsIfBase64Encoded(string input)
{
if (input == null)
{
throw new ArgumentNullException(nameof(input), "Input string cannot be null.");
}
if (input.Length == 0)
{
return string.Empty;
}
byte[] decodedBytes = Convert.FromBase64String(input);
string decodedString = Encoding.UTF8.GetString(decodedBytes);
decodedString = ScrapVsoCommands(decodedString);
return Convert.ToBase64String(Encoding.UTF8.GetBytes(decodedString));
}

private static string ScrapVsoCommands(string input)
{
return Regex.Replace(input, "##vso", "**vso", RegexOptions.IgnoreCase);
}
}
Expand Down
56 changes: 55 additions & 1 deletion src/Test/L0/Util/StringUtilL0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// Licensed under the MIT License.

using Microsoft.VisualStudio.Services.Agent.Util;
using System;
using System.Text;
using System.Globalization;
using Xunit;

Expand All @@ -22,14 +24,66 @@ public class StringUtilL0
[InlineData("##VsO", "**vso")]
[InlineData("", "")]
[InlineData(null, "")]
[InlineData(" ", " ")]
[InlineData(" ", "")]
public void DeactivateVsoCommandsFromStringTest(string input, string expected)
{
var result = StringUtil.DeactivateVsoCommands(input);

Assert.Equal(expected, result);
}

/// <summary>
/// In this test, a vso command is encoded as a bse64 string and being passed as input to the DeactivateBase64EncodedVsoCommands
/// The returned string when decoded will have ## replaced with **, deactivating the vso command.
/// </summary>
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void DeactivateVsoCommandsIfBase64Encoded_EncodedVsoCommands_Returns_DeactivatedVsoCommands()
{
string vsoCommand = "##vso[task.setvariable variable=downloadUrl]https://www.evil.com";
string encodedVsoCommand = Convert.ToBase64String(Encoding.UTF8.GetBytes(vsoCommand));

string result = StringUtil.DeactivateVsoCommandsIfBase64Encoded(encodedVsoCommand);

string deactivatedVsoCommand = "**vso[task.setvariable variable=downloadUrl]https://www.evil.com";
var expected = Convert.ToBase64String(Encoding.UTF8.GetBytes(deactivatedVsoCommand));

Assert.Equal(expected, result);
}

/// <summary>
/// In this test, a vso command is being passed as input to the DeactivateBase64EncodedVsoCommands
/// The unmodified string would be returned.
/// </summary>
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void DeactivateVsoCommandsIfBase64Encoded_NotEncodedVsoCommands_Throws_FormatException()
{
string vsoCommand = "##vso[task.setvariable variable=downloadUrl]https://www.evil.com";
Assert.Throws<FormatException>(() => StringUtil.DeactivateVsoCommandsIfBase64Encoded(vsoCommand));
}

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void DeactivateVsoCommandsIfBase64Encoded_InputEmpty_Returns_UnmodifiedString()
{
string vsoCommand = "";
string result = StringUtil.DeactivateVsoCommandsIfBase64Encoded(vsoCommand);
Assert.Equal(vsoCommand, result);
}

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void DeactivateVsoCommandsIfBase64Encoded_InputNull_Throws_Exception()
{
string vsoCommand = null;
Assert.Throws<ArgumentNullException>(() => StringUtil.DeactivateVsoCommandsIfBase64Encoded(vsoCommand));
}

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
Expand Down
29 changes: 27 additions & 2 deletions src/Test/L0/Worker/WorkerL0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Xunit;
using Microsoft.VisualStudio.Services.WebApi;
using Pipelines = Microsoft.TeamFoundation.DistributedTask.Pipelines;
using System.Text;

namespace Microsoft.VisualStudio.Services.Agent.Tests.Worker
{
Expand Down Expand Up @@ -295,13 +296,37 @@ public void VerifyJobRequestMessageVsoCommandsDeactivatedIfVariableCasesHandlesN

message.Variables[Constants.Variables.Build.SourceVersionMessage] = "";
message.Variables[Constants.Variables.System.SourceVersionMessage] = null;
message.Variables[Constants.Variables.Build.DefinitionName] = " ";
message.Variables[Constants.Variables.Build.DefinitionName] = "";

var scrubbedMessage = WorkerUtilities.DeactivateVsoCommandsFromJobMessageVariables(message);

Assert.Equal("", scrubbedMessage.Variables[Constants.Variables.Build.SourceVersionMessage]);
Assert.Equal("", scrubbedMessage.Variables[Constants.Variables.System.SourceVersionMessage]);
Assert.Equal(" ", scrubbedMessage.Variables[Constants.Variables.Build.DefinitionName]);
Assert.Equal("", scrubbedMessage.Variables[Constants.Variables.Build.DefinitionName]);
}


[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void VerifyJobRequestMessageVsoCommandsDeactivatedIfVariableCasesHandlesBase64EncodedVsoCommands()
{
Pipelines.AgentJobRequestMessage message = CreateJobRequestMessage("jobWithVsoCommands");
// Set up
// A build variable is assigned a VSO command encoded as base 64 string
string vsoCommand = "##vso[task.setvariable variable=downloadUrl]https://www.evil.com";
string encodedVsoCommand = Convert.ToBase64String(Encoding.UTF8.GetBytes(vsoCommand));
message.Variables[Constants.Variables.Build.SourceVersionMessage] = encodedVsoCommand;

// Act
var scrubbedMessage = WorkerUtilities.DeactivateVsoCommandsFromJobMessageVariables(message);

// Expected
// Returned string in it's decode form would have ## replaced with ** to deactivate vso command
string deactivatedVsoCommand = "**vso[task.setvariable variable=downloadUrl]https://www.evil.com";
string expected = Convert.ToBase64String(Encoding.UTF8.GetBytes(deactivatedVsoCommand));

Assert.Equal(expected, scrubbedMessage.Variables[Constants.Variables.Build.SourceVersionMessage]);
}

private bool IsMessageIdentical(Pipelines.AgentJobRequestMessage source, Pipelines.AgentJobRequestMessage target)
Expand Down
Loading