commit d955500acdf5ff707f7e1fa915929b75f06fc584
Author: Iskren Chernev <me@iskren.info>
Date:   Tue Jul 5 14:09:24 2022 +0300

    Improve data pipeline
    
    Thanks to Michael V. Scovetta from Alpha-Omega (part of Open Source
    Security Foundation) for reporting.
    
    TL;DR; build scripts were using node's exec with a single string for
    command and arguments, which could lead to arbitrary command execution
    from the build script (if you pass a specific parameter to a grunt
    task).
    
    New code uses execFile, which has separate command and args, and also
    uses IANA's HTTPS endpoints, so no MITM is possible while downloading
    the latest tzdata.
    
    Security advisory link: https://github.com/openssf-omega/vulnerability-reports/security/advisories/GHSA-qgxp-x7vh-545j

diff --git a/tasks/data-download.js b/tasks/data-download.js
index f606cb2..1307878 100644
--- a/tasks/data-download.js
+++ b/tasks/data-download.js
@@ -1,32 +1,33 @@
 "use strict";
 
 var path = require('path'),
-	exec = require('child_process').exec;
+	execFile = require('child_process').execFile;
 
 module.exports = function (grunt) {
 	grunt.registerTask('data-download', '1. Download data from iana.org/time-zones.', function (version) {
 		version = version || 'latest';
 
 		var done  = this.async(),
-			src   = 'ftp://ftp.iana.org/tz/tzdata-latest.tar.gz',
+			src   = (version === 'latest' ?
+                                'https://data.iana.org/time-zones/tzdata-latest.tar.gz' :
+                                'https://data.iana.org/time-zones/releases/tzdata' + version + '.tar.gz'),
 			curl  = path.resolve('temp/curl', version, 'data.tar.gz'),
 			dest  = path.resolve('temp/download', version);
 
-		if (version !== 'latest') {
-			src = 'https://data.iana.org/time-zones/releases/tzdata' + version + '.tar.gz';
-		}
-
 		grunt.file.mkdir(path.dirname(curl));
 		grunt.file.mkdir(dest);
 
 		grunt.log.ok('Downloading ' + src);
 
-		exec('curl ' + src + ' -o ' + curl + ' && cd ' + dest + ' && gzip -dc ' + curl + ' | tar -xf -', function (err) {
+		execFile('curl', [src, '-o', curl], function (err) {
 			if (err) { throw err; }
+			grunt.log.ok('Downloaded ' + curl + ', extracting . . .');
+			execFile('tar', ['-xzf', curl], { cwd: dest }, function (err) {
+				if (err) { throw err; }
 
-			grunt.log.ok('Downloaded ' + src);
-
-			done();
+				grunt.log.ok('Extracted ' + dest);
+				done();
+			});
 		});
 	});
-};
\ No newline at end of file
+};
diff --git a/tasks/data-zdump.js b/tasks/data-zdump.js
index c177195..65314b5 100644
--- a/tasks/data-zdump.js
+++ b/tasks/data-zdump.js
@@ -1,14 +1,12 @@
 "use strict";
 
 var path = require('path'),
-	exec = require('child_process').exec;
+	execFile = require('child_process').execFile;
 
 module.exports = function (grunt) {
 	grunt.registerTask('data-zdump', '3. Dump data with zdump(8).', function (version) {
 		version = version || 'latest';
 
-		console.log(path.resolve('zdump'));
-
 		var done      = this.async(),
 			zicBase   = path.resolve('temp/zic', version),
 			zdumpBase = path.resolve('temp/zdump', version),
@@ -34,7 +32,7 @@ module.exports = function (grunt) {
 				src  = path.join(zicBase, file),
 				dest = path.join(zdumpBase, file);
 
-			exec('zdump -v ' + src, { maxBuffer: 20*1024*1024 }, function (err, stdout) {
+			execFile('zdump', ['-v', src], { maxBuffer: 20*1024*1024 }, function (err, stdout) {
 				if (err) { throw err; }
 
 				grunt.file.mkdir(path.dirname(dest));
@@ -42,7 +40,7 @@ module.exports = function (grunt) {
 				if (stdout.length === 0) {
 					// on some systems, when there are no transitions then we have
 					// to get creative to learn the offset and abbreviation
-					exec('zdump UTC ' + src, { maxBuffer: 20*1024*1024 }, function (_err, _stdout) {
+					execFile('zdump', ['UTC', src], { maxBuffer: 20*1024*1024 }, function (_err, _stdout) {
 						if (_err) { throw _err; }
 
 						grunt.file.write(dest + '.zdump', normalizePaths(_stdout));
diff --git a/tasks/data-zic.js b/tasks/data-zic.js
index bd81f7e..7913f16 100644
--- a/tasks/data-zic.js
+++ b/tasks/data-zic.js
@@ -1,7 +1,7 @@
 "use strict";
 
 var path = require('path'),
-	exec = require('child_process').exec;
+	execFile = require('child_process').execFile;
 
 module.exports = function (grunt) {
 	grunt.registerTask('data-zic', '2. Compile data sources with zic(8).', function (version) {
@@ -22,7 +22,10 @@ module.exports = function (grunt) {
 			var file = files.shift(),
 				src = path.resolve('temp/download', version, file);
 
-			exec('zic -d ' + dest + ' ' + src, function (err) {
+			if (!grunt.file.exists(src)) {
+				throw new Error("Can't process " + src + " with zic. File doesn't exist");
+			}
+			execFile('zic', ['-d', dest, src], function (err) {
 				if (err) { throw err; }
 
 				grunt.verbose.ok('Compiled zic ' + version + ':' + file);
