CVE-2020-28490: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection') (CWE-78)
value: The package async-git before 1.13.2 are vulnerable to Command Injection via shell meta-characters (back-ticks). For example: git.reset('atouch HACKEDb')
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.8
Published: 2021-02-18T15:15Z
Repository: https://github.com/omrilotan/async-git
Language: JavaScript
Files changed: helpers/spawn/index.js, index.js, lib/index.js, lib/modified/index.js, lib/remote/index.js, lib/remote/spec.js, lib/reset/index.js, lib/reset/spec.js, lib/tag/index.js, lib/tag/spec.js, lib/unadded/index.js, lib/unadded/spec.js, package.json, spec.js, vulnerabilities/spec.js
Commit date: 2021-01-24 20:52:00 +0200
Commit message: Use spawn with git to avoid shell script vulnerabilities (#14)


Diff: diff --git a/helpers/spawn/index.js b/helpers/spawn/index.js
new file mode 100644
index 0000000..7f108ca
--- /dev/null
+++ b/helpers/spawn/index.js
@@ -0,0 +1,38 @@
+const { spawn } = require('child_process');
+
+/**
+ * Executes git with given arguments string. resolves with the output message
+ * @param  {String} string
+ * @return {Promise}
+ */
+module.exports = string => new Promise(
+	(resolve, reject) => {
+		const git = spawn(
+			'git',
+			string.split(' '),
+		);
+
+		const outputs = { stdout: [], stderr: [] };
+
+		git.stdout.on(
+			'data',
+			data => outputs.stdout.push(data.toString()),
+		);
+
+		git.stderr.on(
+			'data',
+			data => outputs.stderr.push(data.toString()),
+		);
+
+		git.on('exit', code => {
+			switch (code) {
+				case 0:
+					resolve(outputs.stdout.join('').trim());
+					break;
+				default:
+					reject(outputs.stderr.join('').trim());
+					break;
+			}
+		});
+	},
+);
diff --git a/index.js b/index.js
index 5249db0..fb77471 100644
--- a/index.js
+++ b/index.js
@@ -1,9 +1,8 @@
-const exec = require('async-execute');
+const spawn = require('./helpers/spawn');
 
 const {
-	list,
 	modified,
 	remote,
 	reset,
 	tag,
 	unadded,
@@ -28,45 +27,45 @@ const formats = [
 /**
  * Items to pull out of terminal output
  * @type {Array[]}
  */
 const outputs = [
-	[ 'branch', 'git rev-parse --abbrev-ref HEAD' ],
-	[ 'origin', 'git remote get-url origin' ],
+	[ 'branch', 'rev-parse --abbrev-ref HEAD' ],
+	[ 'origin', 'remote get-url origin' ],
 ];
 
 /**
  * Lists to get from multi-line terminal output
  * @type {Array[]}
  */
 const lists = [
-	[ 'changed', 'git diff-tree --no-commit-id --name-only -r HEAD' ],
-	[ 'staged', 'git diff --name-only --cached' ],
-	[ 'tags', 'git tag' ],
-	[ 'unstaged', 'git diff --name-only' ],
-	[ 'untracked', 'git ls-files -o --exclude-standard' ],
+	[ 'changed', 'diff-tree --no-commit-id --name-only -r HEAD' ],
+	[ 'staged', 'diff --name-only --cached' ],
+	[ 'tags', 'tag' ],
+	[ 'unstaged', 'diff --name-only' ],
+	[ 'untracked', 'ls-files -o --exclude-standard' ],
 ];
 
 /**
  * Properties which will become (async) getters
  */
 const getters = Object.assign(
 	{
-		date: async () => new Date(parseInt(await exec('git show -s --format=%at')) * 1000),
+		date: async () => new Date(parseInt(await spawn('show -s --format=%at')) * 1000),
 		name: async () => (await remote()).name,
 		owner: async () => (await remote()).owner,
 		unadded,
-		version: async () => (await exec('git version')).split(' ').pop(),
+		version: async () => (await spawn('version')).split(' ').pop(),
 	},
 	...outputs.map(
-		([ key, value ]) => ({ [key]: exec.bind(null, value) }),
+		([ key, value ]) => ({ [key]: spawn.bind(null, value) }),
 	),
 	...lists.map(
-		([ key, value ]) => ({ [key]: list.bind(null, value) }),
+		([ key, value ]) => ({ [key]: async () => (await spawn(value)).split('\n') }),
 	),
 	...formats.map(
-		([ key, value ]) => ({ [key]: exec.bind(null, `git show -s --format=%${value}`) }),
+		([ key, value ]) => ({ [key]: spawn.bind(null, `show -s --format=%${value}`) }),
 	),
 );
 
 /**
  * Properties which will become (async) functions
diff --git a/lib/index.js b/lib/index.js
index 73d8d0a..3de9933 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -1,7 +1,6 @@
 module.exports = {
-	list: require('./list'),
 	modified: require('./modified'),
 	remote: require('./remote'),
 	reset: require('./reset'),
 	tag: require('./tag'),
 	unadded: require('./unadded'),
diff --git a/lib/list/index.js b/lib/list/index.js
deleted file mode 100644
index 83a42fa..0000000
--- a/lib/list/index.js
+++ /dev/null
@@ -1,15 +0,0 @@
-const exec = require('async-execute');
-
-/**
- * List the output lines from command
- * @return {string[]}
- */
-module.exports = async function list(command) {
-	const output = await exec(command);
-
-	return output
-		.split('\n')
-		.map(item => item.trim())
-		.filter(Boolean)
-	;
-};
diff --git a/lib/list/spec.js b/lib/list/spec.js
deleted file mode 100644
index 19ff095..0000000
--- a/lib/list/spec.js
+++ /dev/null
@@ -1,51 +0,0 @@
-const { clean, override } = abuser(__filename);
-
-const diff = `
-		line 1
-		line 2
-line 3
-
-line 5
-`;
-
-describe('lib/list', async() => {
-	const exec = stub();
-	let list;
-
-	before(() => {
-		clean('.');
-		override('async-execute', exec);
-		exec.returns(diff);
-		list = require('.').bind(null, 'some command');
-	});
-	afterEach(() => exec.resetHistory());
-	after(() => clean('.'));
-
-	it('Should call exec with passed command', async() => {
-		await list();
-		expect(exec).to.have.been.calledWith('some command');
-	});
-	it('Should hard reset to a given sha', async() => {
-		const result = await list();
-		expect(result).to.deep.equal([
-			'line 1',
-			'line 2',
-			'line 3',
-			'line 5',
-		]);
-	});
-	it('Should return an empty array for empty output', async() => {
-		exec.returns('');
-		const result = await list();
-		expect(result).to.be.an('array');
-		expect(result).to.have.lengthOf(0);
-	});
-	it('Should filter out empty lines', async() => {
-		exec.returns(`
-
-		`);
-		const result = await list();
-		expect(result).to.be.an('array');
-		expect(result).to.have.lengthOf(0);
-	});
-});
diff --git a/lib/modified/index.js b/lib/modified/index.js
index d70f34a..d1aa07f 100644
--- a/lib/modified/index.js
+++ b/lib/modified/index.js
@@ -1,8 +1,8 @@
 const { resolve } = require('path');
-const exec = require('async-execute');
 const exist = require('@does/exist');
+const spawn = require('../../helpers/spawn');
 
 /**
  * Create and push a git tag with the last commit message
  * @param  {String} path Path to file
  * @return {Date}
@@ -16,9 +16,9 @@ module.exports = async function(path) {
 
 	if (!(await exist(absolute))) {
 		throw new Error(`Could not find file at path "${absolute}"`);
 	}
 
-	const ts = await exec(`git log -1 --format="%at" -- ${path}`);
+	const ts = await spawn(`log -1 --format="%at" -- ${path}`);
 
 	return new Date(Number(ts) * 1000);
 };
diff --git a/lib/remote/index.js b/lib/remote/index.js
index 07218a0..302e8a3 100644
--- a/lib/remote/index.js
+++ b/lib/remote/index.js
@@ -1,13 +1,13 @@
-const exec = require('async-execute');
+const spawn = require('../../helpers/spawn');
 
 /**
  * Get remote repo name and owner
  * @return {string{}}
  */
 module.exports = async function remote() {
-	const origin = await exec('git remote get-url origin');
+	const origin = await spawn('remote get-url origin');
 	const nosuffix = origin.replace(/\.git$/, '');
 	const [ match ] = nosuffix.match(/[\w-]*\/[\w-]+$/) || [ nosuffix ];
 
 	const [ name, owner = '' ] = match.split('/').reverse();
 
diff --git a/lib/remote/spec.js b/lib/remote/spec.js
index 9a8ee2d..8a05e30 100644
--- a/lib/remote/spec.js
+++ b/lib/remote/spec.js
@@ -1,10 +1,10 @@
 const { clean, override } = abuser(__filename);
 
 function cleanup() {
 	clean('.');
-	clean('async-execute');
+	clean('../../helpers/spawn');
 }
 
 describe('lib/remote', () => {
 	beforeEach(cleanup);
 	afterEach(cleanup);
@@ -21,11 +21,11 @@ describe('lib/remote', () => {
 		'file:///srv/repo_owner1/repo-name.git',
 		'ssh://[username@]repo_owner1/repo-name.git',
 		'ssh://[username@]repo_owner1/repo-name',
 	].forEach(
 		format => it(`should find owner and repo name in ${format}`, async () => {
-			override('async-execute', () => format);
+			override('../../helpers/spawn', () => format);
 
 			const remote = require('.');
 			const { owner, name } = await remote();
 			expect(owner).to.equal('repo_owner1');
 			expect(name).to.equal('repo-name');
@@ -38,11 +38,11 @@ describe('lib/remote', () => {
 		'repo-name',
 	].forEach(
 		format => it(
 			`Still returns reponame when no owner is found (${format})`,
 			async () => {
-				override('async-execute', async () => format);
+				override('../../helpers/spawn', async () => format);
 
 				const remote = require('.');
 				const { owner, name } = await remote();
 				expect(owner).to.equal('');
 				expect(name).to.equal('repo-name');
diff --git a/lib/reset/index.js b/lib/reset/index.js
index dd1250a..cc78817 100644
--- a/lib/reset/index.js
+++ b/lib/reset/index.js
@@ -1,19 +1,25 @@
-const exec = require('async-execute');
+const spawn = require('../../helpers/spawn');
 
 /**
  * Reset current HEAD to the specified state
  * @param  {String|Number}  destination
  * @param  {Boolean} options.hard
  * @return {void}
  */
 module.exports = async function(destination, { hard = true } = {}) {
 	if (destination && typeof destination === 'string') {
-		return await exec(`git reset ${JSON.stringify(destination)} ${hard ? '--hard' : ''}`);
+		try {
+			await spawn(`check-ref-format ${destination}`);
+		} catch (error) {
+			throw new RangeError('can not reset to illegal ref "${destination}"');
+		}
+
+		return await spawn(`reset ${destination} ${hard ? '--hard' : ''}`);
 	}
 
 	if (destination && typeof destination === 'number') {
-		return await exec(`git reset HEAD~${Math.abs(destination)} ${hard ? '--hard' : ''}`);
+		return await spawn(`reset HEAD~${Math.abs(destination)} ${hard ? '--hard' : ''}`);
 	}
 
 	throw new TypeError(`No case for handling destination ${destination} (${typeof destination})`);
 };
diff --git a/lib/reset/spec.js b/lib/reset/spec.js
index f6232f6..09a12ed 100644
--- a/lib/reset/spec.js
+++ b/lib/reset/spec.js
@@ -4,11 +4,11 @@ describe('lib/reset', async() => {
 	const exec = spy();
 	let reset;
 
 	before(() => {
 		clean('.');
-		override('async-execute', exec);
+		override('../../helpers/spawn', exec);
 		reset = require('.');
 	});
 	afterEach(() => exec.resetHistory());
 	after(() => clean('.'));
 
@@ -29,24 +29,24 @@ describe('lib/reset', async() => {
 			}
 		}
 	});
 
 	it('Should hard reset to a given sha', async() => {
-		reset('shaid');
-		expect(exec.getCall(0).args[0]).to.equal('git reset "shaid" --hard');
+		await reset('shaid');
+		expect(exec.getCall(1).args[0]).to.equal('reset shaid --hard');
 	});
 
 	it('Should hard reset to n commits back', async() => {
-		reset(1);
-		expect(exec.getCall(0).args[0]).to.equal('git reset HEAD~1 --hard');
+		await reset(1);
+		expect(exec.getCall(0).args[0]).to.equal('reset HEAD~1 --hard');
 	});
 
 	it('Should hard reset to n commits back with negative value as well', async() => {
-		reset(-3);
-		expect(exec.getCall(0).args[0]).to.equal('git reset HEAD~3 --hard');
+		await reset(-3);
+		expect(exec.getCall(0).args[0]).to.equal('reset HEAD~3 --hard');
 	});
 
 	it('Should reset w/o hard argument', async() => {
-		reset('shaid', { hard: false });
-		expect(exec.getCall(0).args[0].trim()).to.equal('git reset "shaid"');
+		await reset('shaid', { hard: false });
+		expect(exec.getCall(1).args[0].trim()).to.equal('reset shaid');
 	});
 });
diff --git a/lib/tag/index.js b/lib/tag/index.js
index 4d1e784..a4e6e37 100644
--- a/lib/tag/index.js
+++ b/lib/tag/index.js
@@ -1,6 +1,6 @@
-const exec = require('async-execute');
+const spawn = require('../../helpers/spawn');
 
 /**
  * Create and push a git tag with the last commit message
  * @param  {String} tag
  * @return {void}
@@ -11,11 +11,11 @@ module.exports = async function(tag) {
 	}
 
 	const { message, author, email } = this;
 
 	await Promise.all([
-		exec(`git config user.name "${await author}"`),
-		exec(`git config user.email "${await email}"`),
+		spawn(`config user.name "${await author}"`),
+		spawn(`config user.email "${await email}"`),
 	]);
-	await exec(`git tag -a ${JSON.stringify(tag)} -m "${await message}"`);
-	await exec(`git push origin ${JSON.stringify(`refs/tags/${tag}`)}`);
+	await spawn(`tag -a ${tag} -m "${await message}"`);
+	await spawn(`push origin refs/tags/${tag}`);
 };
diff --git a/lib/tag/spec.js b/lib/tag/spec.js
index 528bfc1..24d7bf0 100644
--- a/lib/tag/spec.js
+++ b/lib/tag/spec.js
@@ -4,11 +4,11 @@ const dummy = {};
 describe('lib/tag', async() => {
 	let gitTag;
 
 	before(() => {
 		clean('.');
-		override('async-execute', (...args) => dummy.stub(...args));
+		override('../../helpers/spawn', (...args) => dummy.stub(...args));
 
 		gitTag = require('.').bind({
 			message: 'this is a message',
 			author: 'boaty mcboatface',
 			email: 'boaty@boat.face',
@@ -20,18 +20,18 @@ describe('lib/tag', async() => {
 	it('Should use a given user', async() => {
 		const lines = [];
 		dummy.stub = command => lines.push(command);
 
 		await gitTag('1.1.1');
-		expect(lines).to.include('git config user.name "boaty mcboatface"');
-		expect(lines).to.include('git config user.email "boaty@boat.face"');
+		expect(lines).to.include('config user.name "boaty mcboatface"');
+		expect(lines).to.include('config user.email "boaty@boat.face"');
 	});
 
 	it('Should create a git tag with given message and push it', async() => {
 		const lines = [];
 		dummy.stub = command => lines.push(command);
 
 		await gitTag('1.1.1');
-		expect(lines).to.include('git tag -a "1.1.1" -m "this is a message"');
-		expect(lines).to.include('git push origin "refs/tags/1.1.1"');
+		expect(lines).to.include('tag -a 1.1.1 -m "this is a message"');
+		expect(lines).to.include('push origin refs/tags/1.1.1');
 	});
 });
diff --git a/lib/unadded/index.js b/lib/unadded/index.js
index 1ebeff9..2414e55 100644
--- a/lib/unadded/index.js
+++ b/lib/unadded/index.js
@@ -1,13 +1,13 @@
-const exec = require('async-execute');
+const spawn = require('../../helpers/spawn');
 
 /**
  * List all files that would be added by "add ."
  * @return {string[]}
  */
 module.exports = async function unadded() {
-	const output = await exec('git add --dry-run .');
+	const output = await spawn('add --dry-run .');
 
 	return output
 		.split('\n')
 		.map(extract)
 		.filter(Boolean)
diff --git a/lib/unadded/spec.js b/lib/unadded/spec.js
index 54c442a..740cbaa 100644
--- a/lib/unadded/spec.js
+++ b/lib/unadded/spec.js
@@ -3,11 +3,11 @@ const { clean, override } = abuser(__filename);
 describe('lib/unadded', () => {
 	let unadded;
 
 	before(() => {
 		clean('.');
-		override('async-execute', () => `remove '.gitattributes'
+		override('../../helpers/spawn', () => `remove '.gitattributes'
 add 'apps/post/index.js'
 add 'new file.txt'
 `);
 
 		unadded = require('.');
diff --git a/package.json b/package.json
index 14c62da..d9518df 100644
--- a/package.json
+++ b/package.json
@@ -1,8 +1,8 @@
 {
   "name": "async-git",
-  "version": "1.13.1",
+  "version": "1.13.2",
   "description": "👾 Retrieve data from current git repository",
   "keywords": [
     "git",
     "github",
     "bitbucket",
@@ -23,21 +23,22 @@
     "url": "git+https://github.com/omrilotan/async-git.git"
   },
   "homepage": "https://omrilotan.com/async-git/",
   "main": "index.js",
   "scripts": {
+    "pretest": "echo \"content\" > unadded.txt",
     "test": "mocha '**/spec.js' --require .mocha.js --recursive --exclude 'node_modules'",
     "lint": "eslint . --ext .js"
   },
   "dependencies": {
-    "@does/exist": "^1.1.0",
-    "async-execute": "^1.1.0"
+    "@does/exist": "^1.1.0"
   },
   "devDependencies": {
     "@lets/wait": "^2.0.2",
     "@omrilotan/eslint-config": "^1.1.0",
     "abuser": "^2.0.2",
+    "async-execute": "^1.2.0",
     "chai": "^4.2.0",
     "chai-as-promised": "^7.1.1",
     "deep-equal-in-any-order": "^1.0.21",
     "eslint": "^7.0.0",
     "eslint-plugin-log": "^1.2.3",
diff --git a/spec.js b/spec.js
index d7b58fc..c5c64ac 100644
--- a/spec.js
+++ b/spec.js
@@ -1,9 +1,8 @@
 const exec = require('async-execute');
 
-const { clean, override } = abuser(__filename);
-const list = stub();
+const { clean } = abuser(__filename);
 let git;
 
 const is = Object.defineProperty(
 	{},
 	'ci',
@@ -16,13 +15,10 @@ describe('async-git', async() => {
 	let start;
 
 	before(async() => {
 		clean('.');
 		clean('./lib');
-		override('./lib/list', list);
-		override('./lib/unadded', list);
-		list.returns([ 'a', 'b', 'c' ]);
 		git = require('.');
 
 		if (is.ci) { return; }
 
 		start = await git.sha;
@@ -30,12 +26,10 @@ describe('async-git', async() => {
 		await exec('git add .');
 		await exec('git commit -m "committing all changes before tests"');
 		await exec('echo "content" > unadded.txt');
 	});
 
-	afterEach(() => list.resetHistory());
-
 	after(async() => {
 		clean('.');
 
 		if (is.ci) { return; }
 
@@ -97,34 +91,21 @@ describe('async-git', async() => {
 		parts.forEach(
 			part => expect(part == parseInt(part)).to.be.true,
 		);
 	});
 
-	// Lists
-
-	it('Should call list with relevant git command', async() => {
-		await git.changed;
-		expect(list).to.have.been.calledWith('git diff-tree --no-commit-id --name-only -r HEAD');
-	});
-
 	[
 		'changed',
 		'staged',
 		'tags',
-		'unadded',
 		'untracked',
 	].forEach(member => it(`${member} should retrieve an array`, async () => {
 		const value = await git[member];
 		expect(value).to.be.an('array');
 		expect(value).to.have.lengthOf.at.least(1);
 	}));
 
-	it('changed should call list with relevant git command', async() => {
-		await git.changed;
-		expect(list).to.have.been.calledWith('git diff-tree --no-commit-id --name-only -r HEAD');
-	});
-
 	it('changed should retrieve an array of strings', async () => {
 		const value = await git.changed;
 		expect(value).to.be.an('array');
 		value.forEach(
 			file => expect(file).to.be.a('string'),
diff --git a/vulnerabilities/spec.js b/vulnerabilities/spec.js
index fc312e4..4d5609c 100644
--- a/vulnerabilities/spec.js
+++ b/vulnerabilities/spec.js
@@ -29,18 +29,26 @@ async function softly(fn, ...args) {
 		// ignore
 	}
 }
 
 describe('vulnerabilities', async() => {
+	before(async() => {
+		await wait(100);
+		await softly(unlink, 'HACKED');
+	});
 	afterEach(async() => {
 		await wait(100);
 		await softly(unlink, 'HACKED');
 	});
 	it('shell injection in reset', async() => {
 		await softly(git.reset, '; touch HACKED #');
 		expect(await exists('HACKED')).to.be.false;
 	});
+	it('shell injection in reset', async() => {
+		await softly(git.reset, 'a`touch HACKED`b');
+		expect(await exists('HACKED')).to.be.false;
+	});
 	it('shell injection in tag', async() => {
 		await softly(git.tag, '; touch HACKED #');
 		expect(await exists('HACKED')).to.be.false;
 	});
 });

Vulnerable code: const exec = require('async-execute');
	list,
	[ 'branch', 'git rev-parse --abbrev-ref HEAD' ],
	[ 'origin', 'git remote get-url origin' ],
	[ 'changed', 'git diff-tree --no-commit-id --name-only -r HEAD' ],
	[ 'staged', 'git diff --name-only --cached' ],
	[ 'tags', 'git tag' ],
	[ 'unstaged', 'git diff --name-only' ],
	[ 'untracked', 'git ls-files -o --exclude-standard' ],
		date: async () => new Date(parseInt(await exec('git show -s --format=%at')) * 1000),
		version: async () => (await exec('git version')).split(' ').pop(),
		([ key, value ]) => ({ [key]: exec.bind(null, value) }),
		([ key, value ]) => ({ [key]: list.bind(null, value) }),
		([ key, value ]) => ({ [key]: exec.bind(null, `git show -s --format=%${value}`) }),
	list: require('./list'),
const exec = require('async-execute');

/**
 * List the output lines from command
 * @return {string[]}
 */
module.exports = async function list(command) {
	const output = await exec(command);

	return output
		.split('\n')
		.map(item => item.trim())
		.filter(Boolean)
	;
};
const { clean, override } = abuser(__filename);

const diff = `
		line 1
		line 2
line 3

line 5
`;

describe('lib/list', async() => {
	const exec = stub();
	let list;

	before(() => {
		clean('.');
		override('async-execute', exec);
		exec.returns(diff);
		list = require('.').bind(null, 'some command');
	});
	afterEach(() => exec.resetHistory());
	after(() => clean('.'));

	it('Should call exec with passed command', async() => {
		await list();
		expect(exec).to.have.been.calledWith('some command');
	});
	it('Should hard reset to a given sha', async() => {
		const result = await list();
		expect(result).to.deep.equal([
			'line 1',
			'line 2',
			'line 3',
			'line 5',
		]);
	});
	it('Should return an empty array for empty output', async() => {
		exec.returns('');
		const result = await list();
		expect(result).to.be.an('array');
		expect(result).to.have.lengthOf(0);
	});
	it('Should filter out empty lines', async() => {
		exec.returns(`

		`);
		const result = await list();
		expect(result).to.be.an('array');
		expect(result).to.have.lengthOf(0);
	});
});
const exec = require('async-execute');
	const ts = await exec(`git log -1 --format="%at" -- ${path}`);
const exec = require('async-execute');
	const origin = await exec('git remote get-url origin');
	clean('async-execute');
			override('async-execute', () => format);
				override('async-execute', async () => format);
const exec = require('async-execute');
		return await exec(`git reset ${JSON.stringify(destination)} ${hard ? '--hard' : ''}`);
		return await exec(`git reset HEAD~${Math.abs(destination)} ${hard ? '--hard' : ''}`);
		override('async-execute', exec);
		reset('shaid');
		expect(exec.getCall(0).args[0]).to.equal('git reset "shaid" --hard');
		reset(1);
		expect(exec.getCall(0).args[0]).to.equal('git reset HEAD~1 --hard');
		reset(-3);
		expect(exec.getCall(0).args[0]).to.equal('git reset HEAD~3 --hard');
		reset('shaid', { hard: false });
		expect(exec.getCall(0).args[0].trim()).to.equal('git reset "shaid"');
const exec = require('async-execute');
		exec(`git config user.name "${await author}"`),
		exec(`git config user.email "${await email}"`),
	await exec(`git tag -a ${JSON.stringify(tag)} -m "${await message}"`);
	await exec(`git push origin ${JSON.stringify(`refs/tags/${tag}`)}`);
		override('async-execute', (...args) => dummy.stub(...args));
		expect(lines).to.include('git config user.name "boaty mcboatface"');
		expect(lines).to.include('git config user.email "boaty@boat.face"');
		expect(lines).to.include('git tag -a "1.1.1" -m "this is a message"');
		expect(lines).to.include('git push origin "refs/tags/1.1.1"');
const exec = require('async-execute');
	const output = await exec('git add --dry-run .');
		override('async-execute', () => `remove '.gitattributes'
  "version": "1.13.1",
    "@does/exist": "^1.1.0",
    "async-execute": "^1.1.0"
const { clean, override } = abuser(__filename);
const list = stub();
		override('./lib/list', list);
		override('./lib/unadded', list);
		list.returns([ 'a', 'b', 'c' ]);
	afterEach(() => list.resetHistory());

	// Lists

	it('Should call list with relevant git command', async() => {
		await git.changed;
		expect(list).to.have.been.calledWith('git diff-tree --no-commit-id --name-only -r HEAD');
	});

		'unadded',
	it('changed should call list with relevant git command', async() => {
		await git.changed;
		expect(list).to.have.been.calledWith('git diff-tree --no-commit-id --name-only -r HEAD');
	});

Fixed code: const { spawn } = require('child_process');

/**
 * Executes git with given arguments string. resolves with the output message
 * @param  {String} string
 * @return {Promise}
 */
module.exports = string => new Promise(
	(resolve, reject) => {
		const git = spawn(
			'git',
			string.split(' '),
		);

		const outputs = { stdout: [], stderr: [] };

		git.stdout.on(
			'data',
			data => outputs.stdout.push(data.toString()),
		);

		git.stderr.on(
			'data',
			data => outputs.stderr.push(data.toString()),
		);

		git.on('exit', code => {
			switch (code) {
				case 0:
					resolve(outputs.stdout.join('').trim());
					break;
				default:
					reject(outputs.stderr.join('').trim());
					break;
			}
		});
	},
);
const spawn = require('./helpers/spawn');
	[ 'branch', 'rev-parse --abbrev-ref HEAD' ],
	[ 'origin', 'remote get-url origin' ],
	[ 'changed', 'diff-tree --no-commit-id --name-only -r HEAD' ],
	[ 'staged', 'diff --name-only --cached' ],
	[ 'tags', 'tag' ],
	[ 'unstaged', 'diff --name-only' ],
	[ 'untracked', 'ls-files -o --exclude-standard' ],
		date: async () => new Date(parseInt(await spawn('show -s --format=%at')) * 1000),
		version: async () => (await spawn('version')).split(' ').pop(),
		([ key, value ]) => ({ [key]: spawn.bind(null, value) }),
		([ key, value ]) => ({ [key]: async () => (await spawn(value)).split('\n') }),
		([ key, value ]) => ({ [key]: spawn.bind(null, `show -s --format=%${value}`) }),
const spawn = require('../../helpers/spawn');
	const ts = await spawn(`log -1 --format="%at" -- ${path}`);
const spawn = require('../../helpers/spawn');
	const origin = await spawn('remote get-url origin');
	clean('../../helpers/spawn');
			override('../../helpers/spawn', () => format);
				override('../../helpers/spawn', async () => format);
const spawn = require('../../helpers/spawn');
		try {
			await spawn(`check-ref-format ${destination}`);
		} catch (error) {
			throw new RangeError('can not reset to illegal ref "${destination}"');
		}

		return await spawn(`reset ${destination} ${hard ? '--hard' : ''}`);
		return await spawn(`reset HEAD~${Math.abs(destination)} ${hard ? '--hard' : ''}`);
		override('../../helpers/spawn', exec);
		await reset('shaid');
		expect(exec.getCall(1).args[0]).to.equal('reset shaid --hard');
		await reset(1);
		expect(exec.getCall(0).args[0]).to.equal('reset HEAD~1 --hard');
		await reset(-3);
		expect(exec.getCall(0).args[0]).to.equal('reset HEAD~3 --hard');
		await reset('shaid', { hard: false });
		expect(exec.getCall(1).args[0].trim()).to.equal('reset shaid');
const spawn = require('../../helpers/spawn');
		spawn(`config user.name "${await author}"`),
		spawn(`config user.email "${await email}"`),
	await spawn(`tag -a ${tag} -m "${await message}"`);
	await spawn(`push origin refs/tags/${tag}`);
		override('../../helpers/spawn', (...args) => dummy.stub(...args));
		expect(lines).to.include('config user.name "boaty mcboatface"');
		expect(lines).to.include('config user.email "boaty@boat.face"');
		expect(lines).to.include('tag -a 1.1.1 -m "this is a message"');
		expect(lines).to.include('push origin refs/tags/1.1.1');
const spawn = require('../../helpers/spawn');
	const output = await spawn('add --dry-run .');
		override('../../helpers/spawn', () => `remove '.gitattributes'
  "version": "1.13.2",
    "pretest": "echo \"content\" > unadded.txt",
    "@does/exist": "^1.1.0"
    "async-execute": "^1.2.0",
const { clean } = abuser(__filename);
	before(async() => {
		await wait(100);
		await softly(unlink, 'HACKED');
	});
	it('shell injection in reset', async() => {
		await softly(git.reset, 'a`touch HACKED`b');
		expect(await exists('HACKED')).to.be.false;
	});