Skip to content

Commit

Permalink
M289 hotfix for container escaping (#2135)
Browse files Browse the repository at this point in the history
* Fix escaping of docker envs backport

* create as prerelease

* 2.289.4 release notes

Co-authored-by: Nikola Jokic <[email protected]>
  • Loading branch information
thboop and nikola-jokic authored Sep 20, 2022
1 parent b96b3c5 commit bbcf9f2
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 8 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ jobs:
id: sha_noruntime_noexternals
name: Compute SHA256
working-directory: _package_trims/trim_runtime_externals
- name: Create trimmedpackages.json for ${{ matrix.runtime }}
if: matrix.runtime == 'win-x64'
uses: actions/[email protected]
Expand Down Expand Up @@ -282,6 +282,7 @@ jobs:
release_name: "v${{ steps.releaseNote.outputs.version }}"
body: |
${{ steps.releaseNote.outputs.note }}
prerelease: true

# Upload release assets (full runner packages)
- name: Upload Release Asset (win-x64)
Expand Down
2 changes: 1 addition & 1 deletion releaseNote.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
## Features

## Bugs
- Fixed an issue where websockets failed to successfully close when posting log lines (#1790)
- Fixed an issue where container environment variables names or values could escape the docker command (#2108)


## Windows x64
Expand Down
6 changes: 3 additions & 3 deletions src/Runner.Worker/Container/DockerCommandManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,11 +131,11 @@ public async Task<string> DockerCreate(IExecutionContext context, ContainerInfo
{
if (String.IsNullOrEmpty(env.Value))
{
dockerOptions.Add($"-e \"{env.Key}\"");
dockerOptions.Add(DockerUtil.CreateEscapedOption("-e", env.Key));
}
else
{
dockerOptions.Add($"-e \"{env.Key}={env.Value.Replace("\"", "\\\"")}\"");
dockerOptions.Add(DockerUtil.CreateEscapedOption("-e", env.Key, env.Value));
}
}

Expand Down Expand Up @@ -202,7 +202,7 @@ public async Task<int> DockerRun(IExecutionContext context, ContainerInfo contai
{
// e.g. -e MY_SECRET maps the value into the exec'ed process without exposing
// the value directly in the command
dockerOptions.Add($"-e {env.Key}");
dockerOptions.Add(DockerUtil.CreateEscapedOption("-e", env.Key));
}

// Watermark for GitHub Action environment
Expand Down
44 changes: 43 additions & 1 deletion src/Runner.Worker/Container/DockerUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ namespace GitHub.Runner.Worker.Container
{
public class DockerUtil
{
private static readonly Regex QuoteEscape = new Regex(@"(\\*)" + "\"", RegexOptions.Compiled);
private static readonly Regex EndOfStringEscape = new Regex(@"(\\+)$", RegexOptions.Compiled);

public static List<PortMapping> ParseDockerPort(IList<string> portMappingLines)
{
const string targetPort = "targetPort";
Expand All @@ -17,7 +20,7 @@ public static List<PortMapping> ParseDockerPort(IList<string> portMappingLines)
string pattern = $"^(?<{targetPort}>\\d+)/(?<{proto}>\\w+) -> (?<{host}>.+):(?<{hostPort}>\\d+)$";

List<PortMapping> portMappings = new List<PortMapping>();
foreach(var line in portMappingLines)
foreach (var line in portMappingLines)
{
Match m = Regex.Match(line, pattern, RegexOptions.None, TimeSpan.FromSeconds(1));
if (m.Success)
Expand Down Expand Up @@ -61,5 +64,44 @@ public static string ParseRegistryHostnameFromImageName(string name)
}
return "";
}

public static string CreateEscapedOption(string flag, string key)
{
if (String.IsNullOrEmpty(key))
{
return "";
}
return $"{flag} {EscapeString(key)}";
}

public static string CreateEscapedOption(string flag, string key, string value)
{
if (String.IsNullOrEmpty(key))
{
return "";
}
var escapedString = EscapeString($"{key}={value}");
return $"{flag} {escapedString}";
}

private static string EscapeString(string value)
{
if (String.IsNullOrEmpty(value))
{
return "";
}
// Dotnet escaping rules are weird here, we can only escape \ if it precedes a "
// If a double quotation mark follows two or an even number of backslashes, each proceeding backslash pair is replaced with one backslash and the double quotation mark is removed.
// If a double quotation mark follows an odd number of backslashes, including just one, each preceding pair is replaced with one backslash and the remaining backslash is removed; however, in this case the double quotation mark is not removed.
// https://docs.microsoft.com/en-us/dotnet/api/system.environment.getcommandlineargs?redirectedfrom=MSDN&view=net-6.0#remarks

// First, find any \ followed by a " and double the number of \ + 1.
value = QuoteEscape.Replace(value, @"$1$1\" + "\"");
// Next, what if it ends in `\`, it would escape the end quote. So, we need to detect that at the end of the string and perform the same escape
// Luckily, we can just use the $ character with detects the end of string in regex
value = EndOfStringEscape.Replace(value, @"$1$1");
// Finally, wrap it in quotes
return $"\"{value}\"";
}
}
}
2 changes: 1 addition & 1 deletion src/Runner.Worker/Handlers/StepHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ public async Task<int> ExecuteAsync(string workingDirectory,
{
// e.g. -e MY_SECRET maps the value into the exec'ed process without exposing
// the value directly in the command
dockerCommandArgs.Add($"-e {env.Key}");
dockerCommandArgs.Add(DockerUtil.CreateEscapedOption("-e", env.Key));
}
if (!string.IsNullOrEmpty(PrependPath))
{
Expand Down
49 changes: 49 additions & 0 deletions src/Test/L0/Container/DockerUtilL0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -144,5 +144,54 @@ public void ParseRegistryHostnameFromImageName(string input, string expected)
var actual = DockerUtil.ParseRegistryHostnameFromImageName(input);
Assert.Equal(expected, actual);
}

[Theory]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
[InlineData("", "")]
[InlineData("foo", "foo")]
[InlineData("foo \\ bar", "foo \\ bar")]
[InlineData("foo \\", "foo \\\\")]
[InlineData("foo \\\\", "foo \\\\\\\\")]
[InlineData("foo \\\" bar", "foo \\\\\\\" bar")]
[InlineData("foo \\\\\" bar", "foo \\\\\\\\\\\" bar")]
public void CreateEscapedOption_keyOnly(string input, string escaped)
{
var flag = "--example";
var actual = DockerUtil.CreateEscapedOption(flag, input);
string expected;
if (String.IsNullOrEmpty(input))
{
expected = "";
}
else
{
expected = $"{flag} \"{escaped}\"";
}
Assert.Equal(expected, actual);
}

[Theory]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
[InlineData("foo", "bar", "foo=bar")]
[InlineData("foo\\", "bar", "foo\\=bar")]
[InlineData("foo\\", "bar\\", "foo\\=bar\\\\")]
[InlineData("foo \\","bar \\", "foo \\=bar \\\\")]
public void CreateEscapedOption_keyValue(string keyInput, string valueInput, string escapedString)
{
var flag = "--example";
var actual = DockerUtil.CreateEscapedOption(flag, keyInput, valueInput);
string expected;
if (String.IsNullOrEmpty(keyInput))
{
expected = "";
}
else
{
expected = $"{flag} \"{escapedString}\"";
}
Assert.Equal(expected, actual);
}
}
}
2 changes: 1 addition & 1 deletion src/runnerversion
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.289.3
2.289.4

0 comments on commit bbcf9f2

Please sign in to comment.