Headline
CVE-2022-29184: Improve escaping of arguments when constructing Hg command calls · gocd/gocd@37d3511
GoCD is a continuous delivery server. In GoCD versions prior to 22.1.0, it is possible for existing authenticated users who have permissions to edit or create pipeline materials or pipeline configuration repositories to get remote code execution capability on the GoCD server via configuring a malicious branch name which abuses Mercurial hooks/aliases to exploit a command injection weakness. An attacker would require access to an account with existing GoCD administration permissions to either create/edit (hg
-based) configuration repositories; create/edit pipelines and their (hg
-based) materials; or, where “pipelines-as-code” configuration repositories are used, to commit malicious configuration to such an external repository which will be automatically parsed into a pipeline configuration and (hg
) material definition by the GoCD server. This issue is fixed in GoCD 22.1.0. As a workaround, users who do not use/rely upon Mercurial materials can uninstall/remove the hg
/Mercurial binary from the underlying GoCD Server operating system or Docker image.
@@ -24,8 +24,8 @@ import org.slf4j.LoggerFactory;
import java.io.File; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List;
@@ -52,27 +52,31 @@ public HgCommand(String materialFingerprint, File workingDir, String branch, Str this.secrets = secrets != null ? secrets : new ArrayList<>(); }
private boolean pull(ConsoleOutputStreamConsumer outputStreamConsumer) { CommandLine hg = hg("pull", "-b", branch, "–config", String.format("paths.default=%s", url)); return execute(hg, outputStreamConsumer) == 0; }
public HgVersion version() { CommandLine hg = createCommandLine(“hg”).withArgs(“version”).withEncoding(“utf-8”); CommandLine hg = createCommandLine(“hg”).withArgs(“version”).withEncoding(“UTF-8”); String hgOut = execute(hg, new NamedProcessTag(“hg version check”)).outputAsString(); return HgVersion.parse(hgOut); }
public int clone(ConsoleOutputStreamConsumer outputStreamConsumer, UrlArgument repositoryUrl) { CommandLine hg = createCommandLine(“hg”).withArgs(“clone”).withArg("-b").withArg(branch).withArg(repositoryUrl) .withArg(workingDir.getAbsolutePath()).withNonArgSecrets(secrets).withEncoding(“utf-8”); CommandLine hg = createCommandLine(“hg”) .withArgs(“clone”) .withArg(branchArg()) .withArg(“–”) .withArg(repositoryUrl) .withArg(workingDir.getAbsolutePath()) .withNonArgSecrets(secrets) .withEncoding(“UTF-8”); return execute(hg, outputStreamConsumer); }
public void checkConnection(UrlArgument repositoryURL) { execute(createCommandLine(“hg”).withArgs("id", “–id”).withArg(repositoryURL).withNonArgSecrets(secrets).withEncoding(“utf-8”), new NamedProcessTag(repositoryURL.forDisplay())); CommandLine hg = createCommandLine(“hg”) .withArgs("id", "–id", “–”) .withArg(repositoryURL) .withNonArgSecrets(secrets) .withEncoding(“UTF-8”); execute(hg, new NamedProcessTag(repositoryURL.forDisplay())); }
public void updateTo(Revision revision, ConsoleOutputStreamConsumer outputStreamConsumer) { @@ -81,6 +85,11 @@ public void updateTo(Revision revision, ConsoleOutputStreamConsumer outputStream } }
private boolean pull(ConsoleOutputStreamConsumer outputStreamConsumer) { CommandLine hg = hg("pull", branchArg(), "–config", String.format("paths.default=%s", url)); return execute(hg, outputStreamConsumer) == 0; }
private boolean update(Revision revision, ConsoleOutputStreamConsumer outputStreamConsumer) { CommandLine hg = hg("update", "–clean", "-r", revision.getRevision()); return execute(hg, outputStreamConsumer) == 0; @@ -105,36 +114,29 @@ public List<Modification> latestOneModificationAsModifications() { return findRecentModifications(1); }
private String templatePath() { if (templatePath == null) { String file = HgCommand.class.getResource(“/hg.template”).getFile(); try { templatePath = URLDecoder.decode(new File(file).getAbsolutePath(), “UTF-8”); } catch (UnsupportedEncodingException e) { templatePath = URLDecoder.decode(new File(file).getAbsolutePath()); } } return templatePath; }
List<Modification> findRecentModifications(int count) { private List<Modification> findRecentModifications(int count) { // Currently impossible to check modifications on a remote repository. InMemoryStreamConsumer consumer = inMemoryConsumer(); bombUnless(pull(consumer), "Failed to run hg pull command: " + consumer.getAllOutput()); CommandLine hg = hg("log", "–limit", String.valueOf(count), "-b", branch, "–style", templatePath()); CommandLine hg = hg("log", "–limit", String.valueOf(count), branchArg(), "–style", templatePath()); return new HgModificationSplitter(execute(hg)).modifications(); }
public List<Modification> modificationsSince(Revision revision) { InMemoryStreamConsumer consumer = inMemoryConsumer(); bombUnless(pull(consumer), "Failed to run hg pull command: " + consumer.getAllOutput()); CommandLine hg = hg("log", "-r", “tip:” + revision.getRevision(), "-b", branch, "–style", templatePath()); CommandLine hg = hg("log", "-r", “tip:” + revision.getRevision(), branchArg(), "–style", templatePath()); return new HgModificationSplitter(execute(hg)).filterOutRevision(revision); }
private String templatePath() { if (templatePath == null) { String file = HgCommand.class.getResource(“/hg.template”).getFile(); templatePath = URLDecoder.decode(new File(file).getAbsolutePath(), StandardCharsets.UTF_8); } return templatePath; }
public ConsoleResult workingRepositoryUrl() { CommandLine hg = hg("showconfig", “paths.default”);
@@ -144,6 +146,10 @@ public ConsoleResult workingRepositoryUrl() { return result; }
private String branchArg() { return “–branch=” + branch; }
private CommandLine hg(String… arguments) { return createCommandLine(“hg”).withArgs(arguments).withNonArgSecrets(secrets).withWorkingDir(workingDir).withEncoding(“UTF-8”); }