CVE-2020-15272: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection') (CWE-78)
value: In the git-tag-annotation-action (open source GitHub Action) before version 1.0.1, an attacker can execute arbitrary (*) shell commands if they can control the value of [the `tag` input] or manage to alter the value of [the `GITHUB_REF` environment variable]. The problem has been patched in version 1.0.1. If you don't use the `tag` input you are most likely safe. The `GITHUB_REF` environment variable is protected by the GitHub Actions environment so attacks from there should be impossible. If you must use the `tag` input and cannot upgrade to `> 1.0.0` make sure that the value is not controlled by another Action.
CWE description: The product constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component.
CVSS v3 base score: 9.6
Published: 2020-10-26T19:15Z
Repository: https://github.com/ericcornelissen/git-tag-annotation-action
Language: JavaScript
Files changed: CHANGELOG.md, action.yml, src/main.js, test/main.test.js
Commit date: 2020-10-24 21:29:06 +0300
Commit message: Fix shell injection bug

Fix a shell injection bug where an attacker controlling the "tag" value
in the code (i.e. the value of `process.env.GITHUB_REF` or of the
`with: tag` input value) can run arbitrary (*) shell commands.

Also, update the Action's branding, changing the Action icon from
"git-commit" to "tag".

(*): provided it is within the limits of the GitHub Actions environment

Diff: diff --git a/CHANGELOG.md b/CHANGELOG.md
index 38e9021..ee2af5f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,10 +7,15 @@ Versioning].
 
 ## [Unreleased]
 
 - _No changes yet_
 
+## [1.0.1] - 2020-10-24
+
+- Update Action icon.
+- Fix shell injection vulnerability.
+
 ## [1.0.0] - 2020-10-22
 
 - Run the Action to get the git tag annotation of the current tag.
 - Run the Action to get the git tag annotation of a specified tag.
 
diff --git a/action.yml b/action.yml
index bad74ed..0fe296c 100644
--- a/action.yml
+++ b/action.yml
@@ -14,7 +14,7 @@ outputs:
 runs:
   using: 'node12'
   main: 'lib/index.js'
 
 branding:
-  icon: 'git-commit'
+  icon: 'tag'
   color: 'gray-dark'
diff --git a/src/main.js b/src/main.js
index 1beaaed..18522b1 100644
--- a/src/main.js
+++ b/src/main.js
@@ -1,22 +1,30 @@
 const core = require('@actions/core');
 const { exec } = require('child_process');
 
+// Based on https://stackoverflow.com/a/22827128
+function escapeShellArg(arg) {
+  return arg.replace(/'/g, `'\\''`);
+}
+
 function main() {
   try {
     let tag = process.env.GITHUB_REF;
     if (core.getInput('tag')) {
       tag = `refs/tags/${core.getInput('tag')}`;
     }
 
-    exec(`git for-each-ref --format='%(contents)' ${tag}`, (err, stdout) => {
-      if (err) {
-        core.setFailed(err);
-      } else {
-        core.setOutput('git-tag-annotation', stdout);
-      }
-    });
+    exec(
+      `git for-each-ref --format='%(contents)' '${escapeShellArg(tag)}'`,
+      (err, stdout) => {
+        if (err) {
+          core.setFailed(err);
+        } else {
+          core.setOutput('git-tag-annotation', stdout);
+        }
+      },
+    );
   } catch (error) {
     core.setFailed(error.message);
   }
 }
 
diff --git a/test/main.test.js b/test/main.test.js
index f827455..47c3538 100644
--- a/test/main.test.js
+++ b/test/main.test.js
@@ -21,11 +21,11 @@ it.each([
   process.env.GITHUB_REF = `refs/tags/${tag}`;
 
   main();
 
   expect(child_process.exec).toHaveBeenCalledWith(
-    `git for-each-ref --format='%(contents)' refs/tags/${tag}`,
+    `git for-each-ref --format='%(contents)' 'refs/tags/${tag}'`,
     expect.any(Function),
   );
 });
 
 it('tries to get a tag from the input', () => {
@@ -44,11 +44,11 @@ it.each([
 
   main();
 
   expect(core.getInput).toHaveBeenCalledTimes(2);
   expect(child_process.exec).toHaveBeenCalledWith(
-    `git for-each-ref --format='%(contents)' refs/tags/${tag}`,
+    `git for-each-ref --format='%(contents)' 'refs/tags/${tag}'`,
     expect.any(Function),
   );
 });
 
 it('outputs the annotation', (done) => {
@@ -87,5 +87,16 @@ it('sets an error if exec fails', () => {
   main();
 
   expect(core.setOutput).not.toHaveBeenCalled();
   expect(core.setFailed).toHaveBeenCalledTimes(1);
 });
+
+it('escapes malicious values from the input', () => {
+  core.getInput.mockReturnValue(`'; $(cat /etc/shadow)`);
+
+  main();
+
+  expect(child_process.exec).toHaveBeenCalledWith(
+    "git for-each-ref --format='%(contents)' 'refs/tags/'\\''; $(cat /etc/shadow)'",
+    expect.any(Function),
+  );
+});

Vulnerable code:   icon: 'git-commit'
    exec(`git for-each-ref --format='%(contents)' ${tag}`, (err, stdout) => {
      if (err) {
        core.setFailed(err);
      } else {
        core.setOutput('git-tag-annotation', stdout);
      }
    });
    `git for-each-ref --format='%(contents)' refs/tags/${tag}`,
    `git for-each-ref --format='%(contents)' refs/tags/${tag}`,
Fixed code: ## [1.0.1] - 2020-10-24

- Update Action icon.
- Fix shell injection vulnerability.

  icon: 'tag'
// Based on https://stackoverflow.com/a/22827128
function escapeShellArg(arg) {
  return arg.replace(/'/g, `'\\''`);
}

    exec(
      `git for-each-ref --format='%(contents)' '${escapeShellArg(tag)}'`,
      (err, stdout) => {
        if (err) {
          core.setFailed(err);
        } else {
          core.setOutput('git-tag-annotation', stdout);
        }
      },
    );
    `git for-each-ref --format='%(contents)' 'refs/tags/${tag}'`,
    `git for-each-ref --format='%(contents)' 'refs/tags/${tag}'`,

it('escapes malicious values from the input', () => {
  core.getInput.mockReturnValue(`'; $(cat /etc/shadow)`);

  main();

  expect(child_process.exec).toHaveBeenCalledWith(
    "git for-each-ref --format='%(contents)' 'refs/tags/'\\''; $(cat /etc/shadow)'",
    expect.any(Function),
  );
});