Skip to content
Merged
38 changes: 38 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,43 @@ public static string DeactivateVsoCommands(string input)
return string.Empty;
}

input = DeactivateVsoCommandsIfBase64Encoded(input);
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 and input is returned unmodified.
/// </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[] buffer = new byte[input.Length];
if (Convert.TryFromBase64String(input, buffer, out int bytesWritten))
{
string decodedString = Encoding.UTF8.GetString(buffer, 0, bytesWritten);
decodedString = ScrapVsoCommands(decodedString);
return Convert.ToBase64String(Encoding.UTF8.GetBytes(decodedString));
}
else
{
return input;
}
}

private static string ScrapVsoCommands(string input)
{
return Regex.Replace(input, "##vso", "**vso", RegexOptions.IgnoreCase);
}
}
Expand Down
57 changes: 56 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,67 @@ 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_Returns_UnmodifiedVsoCommands()
{
string vsoCommand = "##vso[task.setvariable variable=downloadUrl]https://www.evil.com";
string result = StringUtil.DeactivateVsoCommandsIfBase64Encoded(vsoCommand);
Assert.Equal(vsoCommand, result);
}

[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 @@ -293,13 +294,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