Revert "Fixed a bug where a misplaced = character could bypass heredoc-style processing. (#2627)" (#2774)

This reverts commit 4ffd081aea.
This commit is contained in:
Cory Miller
2023-08-16 15:03:55 -04:00
committed by GitHub
parent e94e744bed
commit 460d9ae5a8
7 changed files with 1008 additions and 659 deletions

View File

@@ -1,36 +1,44 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Container;
using GitHub.Runner.Worker.Handlers;
using Moq;
using Xunit;
using DTWebApi = GitHub.DistributedTask.WebApi;
namespace GitHub.Runner.Common.Tests.Worker
{
public sealed class SetOutputFileCommandL0 : FileCommandTestBase<SetOutputFileCommand>
public sealed class SetOutputFileCommandL0
{
protected override IDictionary<string, string> PostSetup()
{
var outputs = new Dictionary<string, string>();
var reference = string.Empty;
_executionContext.Setup(x => x.SetOutput(It.IsAny<string>(), It.IsAny<string>(), out reference))
.Callback((string name, string value, out string reference) =>
{
reference = value;
outputs[name] = value;
});
return outputs;
}
private Mock<IExecutionContext> _executionContext;
private List<Tuple<DTWebApi.Issue, string>> _issues;
private Dictionary<string, string> _outputs;
private string _rootDirectory;
private SetOutputFileCommand _setOutputFileCommand;
private ITraceWriter _trace;
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void SetOutputFileCommand_DirectoryNotFound()
{
base.TestDirectoryNotFound();
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "directory-not-found", "env");
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(0, _outputs.Count);
}
}
[Fact]
@@ -38,7 +46,13 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_NotFound()
{
base.TestNotFound();
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "file-not-found");
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(0, _outputs.Count);
}
}
[Fact]
@@ -46,7 +60,15 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_EmptyFile()
{
base.TestEmptyFile();
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "empty-file");
var content = new List<string>();
WriteContent(stateFile, content);
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(0, _outputs.Count);
}
}
[Fact]
@@ -54,7 +76,19 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Simple()
{
base.TestSimple();
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "simple");
var content = new List<string>
{
"MY_OUTPUT=MY VALUE",
};
WriteContent(stateFile, content);
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(1, _outputs.Count);
Assert.Equal("MY VALUE", _outputs["MY_OUTPUT"]);
}
}
[Fact]
@@ -62,7 +96,24 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Simple_SkipEmptyLines()
{
base.TestSimple_SkipEmptyLines();
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "simple");
var content = new List<string>
{
string.Empty,
"MY_OUTPUT=my value",
string.Empty,
"MY_OUTPUT_2=my second value",
string.Empty,
};
WriteContent(stateFile, content);
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(2, _outputs.Count);
Assert.Equal("my value", _outputs["MY_OUTPUT"]);
Assert.Equal("my second value", _outputs["MY_OUTPUT_2"]);
}
}
[Fact]
@@ -70,7 +121,19 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Simple_EmptyValue()
{
base.TestSimple_EmptyValue();
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "simple-empty-value");
var content = new List<string>
{
"MY_OUTPUT=",
};
WriteContent(stateFile, content);
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(1, _outputs.Count);
Assert.Equal(string.Empty, _outputs["MY_OUTPUT"]);
}
}
[Fact]
@@ -78,7 +141,23 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Simple_MultipleValues()
{
base.TestSimple_MultipleValues();
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "simple");
var content = new List<string>
{
"MY_OUTPUT=my value",
"MY_OUTPUT_2=",
"MY_OUTPUT_3=my third value",
};
WriteContent(stateFile, content);
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(3, _outputs.Count);
Assert.Equal("my value", _outputs["MY_OUTPUT"]);
Assert.Equal(string.Empty, _outputs["MY_OUTPUT_2"]);
Assert.Equal("my third value", _outputs["MY_OUTPUT_3"]);
}
}
[Fact]
@@ -86,7 +165,23 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Simple_SpecialCharacters()
{
base.TestSimple_SpecialCharacters();
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "simple");
var content = new List<string>
{
"MY_OUTPUT==abc",
"MY_OUTPUT_2=def=ghi",
"MY_OUTPUT_3=jkl=",
};
WriteContent(stateFile, content);
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(3, _outputs.Count);
Assert.Equal("=abc", _outputs["MY_OUTPUT"]);
Assert.Equal("def=ghi", _outputs["MY_OUTPUT_2"]);
Assert.Equal("jkl=", _outputs["MY_OUTPUT_3"]);
}
}
[Fact]
@@ -94,7 +189,23 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc()
{
base.TestHeredoc();
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
"MY_OUTPUT<<EOF",
"line one",
"line two",
"line three",
"EOF",
};
WriteContent(stateFile, content);
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(1, _outputs.Count);
Assert.Equal($"line one{Environment.NewLine}line two{Environment.NewLine}line three", _outputs["MY_OUTPUT"]);
}
}
[Fact]
@@ -102,7 +213,20 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc_EmptyValue()
{
base.TestHeredoc_EmptyValue();
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
"MY_OUTPUT<<EOF",
"EOF",
};
WriteContent(stateFile, content);
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(1, _outputs.Count);
Assert.Equal(string.Empty, _outputs["MY_OUTPUT"]);
}
}
[Fact]
@@ -110,52 +234,73 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc_SkipEmptyLines()
{
base.TestHeredoc_SkipEmptyLines();
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
string.Empty,
"MY_OUTPUT<<EOF",
"hello",
"world",
"EOF",
string.Empty,
"MY_OUTPUT_2<<EOF",
"HELLO",
"AGAIN",
"EOF",
string.Empty,
};
WriteContent(stateFile, content);
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(2, _outputs.Count);
Assert.Equal($"hello{Environment.NewLine}world", _outputs["MY_OUTPUT"]);
Assert.Equal($"HELLO{Environment.NewLine}AGAIN", _outputs["MY_OUTPUT_2"]);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc_EdgeCases()
public void SetOutputFileCommand_Heredoc_SpecialCharacters()
{
base.TestHeredoc_EdgeCases();
}
[Theory]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
// All of the following are not only valid, but quite plausible end markers.
// Most are derived straight from the example at https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
#pragma warning disable format
[InlineData("=EOF")][InlineData("==EOF")][InlineData("EO=F")][InlineData("EO==F")][InlineData("EOF=")][InlineData("EOF==")]
[InlineData("<EOF")][InlineData("<<EOF")][InlineData("EO<F")][InlineData("EO<<F")][InlineData("EOF<")][InlineData("EOF<<")]
[InlineData("+EOF")][InlineData("++EOF")][InlineData("EO+F")][InlineData("EO++F")][InlineData("EOF+")][InlineData("EOF++")]
[InlineData("/EOF")][InlineData("//EOF")][InlineData("EO/F")][InlineData("EO//F")][InlineData("EOF/")][InlineData("EOF//")]
#pragma warning restore format
[InlineData("<<//++==")]
[InlineData("contrivedBase64==")]
[InlineData("khkIhPxsVA==")]
[InlineData("D+Y8zE/EOw==")]
[InlineData("wuOWG4S6FQ==")]
[InlineData("7wigCJ//iw==")]
[InlineData("uifTuYTs8K4=")]
[InlineData("M7N2ITg/04c=")]
[InlineData("Xhh+qp+Y6iM=")]
[InlineData("5tdblQajc/b+EGBZXo0w")]
[InlineData("jk/UMjIx/N0eVcQYOUfw")]
[InlineData("/n5lsw73Cwl35Hfuscdz")]
[InlineData("ZvnAEW+9O0tXp3Fmb3Oh")]
public void SetOutputFileCommand_Heredoc_EndMarkerVariations(string validEndMarker)
{
base.TestHeredoc_EndMarkerVariations(validEndMarker);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc_EqualBeforeMultilineIndicator()
{
base.TestHeredoc_EqualBeforeMultilineIndicator();
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
"MY_OUTPUT<<=EOF",
"hello",
"one",
"=EOF",
"MY_OUTPUT_2<<<EOF",
"hello",
"two",
"<EOF",
"MY_OUTPUT_3<<EOF",
"hello",
string.Empty,
"three",
string.Empty,
"EOF",
"MY_OUTPUT_4<<EOF",
"hello=four",
"EOF",
"MY_OUTPUT_5<<EOF",
" EOF",
"EOF",
};
WriteContent(stateFile, content);
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(5, _outputs.Count);
Assert.Equal($"hello{Environment.NewLine}one", _outputs["MY_OUTPUT"]);
Assert.Equal($"hello{Environment.NewLine}two", _outputs["MY_OUTPUT_2"]);
Assert.Equal($"hello{Environment.NewLine}{Environment.NewLine}three{Environment.NewLine}", _outputs["MY_OUTPUT_3"]);
Assert.Equal($"hello=four", _outputs["MY_OUTPUT_4"]);
Assert.Equal($" EOF", _outputs["MY_OUTPUT_5"]);
}
}
[Fact]
@@ -163,7 +308,21 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc_MissingNewLine()
{
base.TestHeredoc_MissingNewLine();
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
"MY_OUTPUT<<EOF",
"line one",
"line two",
"line three",
"EOF",
};
WriteContent(stateFile, content, " ");
var ex = Assert.Throws<Exception>(() => _setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null));
Assert.Contains("Matching delimiter not found", ex.Message);
}
}
[Fact]
@@ -171,7 +330,21 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc_MissingNewLineMultipleLines()
{
base.TestHeredoc_MissingNewLineMultipleLines();
using (var hostContext = Setup())
{
var stateFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
"MY_OUTPUT<<EOF",
@"line one
line two
line three",
"EOF",
};
WriteContent(stateFile, content, " ");
var ex = Assert.Throws<Exception>(() => _setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null));
Assert.Contains("EOF marker missing new line", ex.Message);
}
}
#if OS_WINDOWS
@@ -180,9 +353,96 @@ namespace GitHub.Runner.Common.Tests.Worker
[Trait("Category", "Worker")]
public void SetOutputFileCommand_Heredoc_PreservesNewline()
{
base.TestHeredoc_PreservesNewline();
using (var hostContext = Setup())
{
var newline = "\n";
var stateFile = Path.Combine(_rootDirectory, "heredoc");
var content = new List<string>
{
"MY_OUTPUT<<EOF",
"hello",
"world",
"EOF",
};
WriteContent(stateFile, content, newline: newline);
_setOutputFileCommand.ProcessCommand(_executionContext.Object, stateFile, null);
Assert.Equal(0, _issues.Count);
Assert.Equal(1, _outputs.Count);
Assert.Equal($"hello{newline}world", _outputs["MY_OUTPUT"]);
}
}
#endif
private void WriteContent(
string path,
List<string> content,
string newline = null)
{
if (string.IsNullOrEmpty(newline))
{
newline = Environment.NewLine;
}
var encoding = new UTF8Encoding(true); // Emit BOM
var contentStr = string.Join(newline, content);
File.WriteAllText(path, contentStr, encoding);
}
private TestHostContext Setup([CallerMemberName] string name = "")
{
_issues = new List<Tuple<DTWebApi.Issue, string>>();
_outputs = new Dictionary<string, string>();
var hostContext = new TestHostContext(this, name);
// Trace
_trace = hostContext.GetTrace();
// Directory for test data
var workDirectory = hostContext.GetDirectory(WellKnownDirectory.Work);
ArgUtil.NotNullOrEmpty(workDirectory, nameof(workDirectory));
Directory.CreateDirectory(workDirectory);
_rootDirectory = Path.Combine(workDirectory, nameof(SetOutputFileCommandL0));
Directory.CreateDirectory(_rootDirectory);
// Execution context
_executionContext = new Mock<IExecutionContext>();
_executionContext.Setup(x => x.Global)
.Returns(new GlobalContext
{
EnvironmentVariables = new Dictionary<string, string>(VarUtil.EnvironmentVariableKeyComparer),
WriteDebug = true,
});
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.Issue>(), It.IsAny<ExecutionContextLogOptions>()))
.Callback((DTWebApi.Issue issue, ExecutionContextLogOptions logOptions) =>
{
var resolvedMessage = issue.Message;
if (logOptions.WriteToLog && !string.IsNullOrEmpty(logOptions.LogMessageOverride))
{
resolvedMessage = logOptions.LogMessageOverride;
}
_issues.Add(new(issue, resolvedMessage));
_trace.Info($"Issue '{issue.Type}': {resolvedMessage}");
});
_executionContext.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
.Callback((string tag, string message) =>
{
_trace.Info($"{tag}{message}");
});
var reference = string.Empty;
_executionContext.Setup(x => x.SetOutput(It.IsAny<string>(), It.IsAny<string>(), out reference))
.Callback((string name, string value, out string reference) =>
{
reference = value;
_outputs[name] = value;
});
// SetOutputFileCommand
_setOutputFileCommand = new SetOutputFileCommand();
_setOutputFileCommand.Initialize(hostContext);
return hostContext;
}
}
}