Skip to content

Commit fc57b6f

Browse files
authored
Harden creation of child processes (#55697) (#59801)
Add general protection against RCE vulnerabilities similar to the one described in CVE-2019-7609. Closes #49605
1 parent 63a2d44 commit fc57b6f

File tree

12 files changed

+809
-10
lines changed

12 files changed

+809
-10
lines changed

.eslintrc.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,16 @@ module.exports = {
518518
},
519519
},
520520

521+
/**
522+
* Harden specific rules
523+
*/
524+
{
525+
files: ['test/harden/*.js'],
526+
rules: {
527+
'mocha/handle-done-callback': 'off', // TODO: Find a way to disable all mocha rules
528+
},
529+
},
530+
521531
/**
522532
* APM overrides
523533
*/

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@
243243
"regenerator-runtime": "^0.13.3",
244244
"regression": "2.0.1",
245245
"request": "^2.88.0",
246+
"require-in-the-middle": "^5.0.2",
246247
"reselect": "^4.0.0",
247248
"resize-observer-polyfill": "^1.5.0",
248249
"rison-node": "1.0.2",
@@ -474,6 +475,7 @@
474475
"strip-ansi": "^3.0.1",
475476
"supertest": "^3.1.0",
476477
"supertest-as-promised": "^4.0.2",
478+
"tape": "^4.13.0",
477479
"tree-kill": "^1.2.2",
478480
"typescript": "3.7.2",
479481
"typings-tester": "^0.3.2",

scripts/test_hardening.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
var execFileSync = require('child_process').execFileSync;
21+
var path = require('path');
22+
var syncGlob = require('glob').sync;
23+
var program = require('commander');
24+
25+
program
26+
.name('node scripts/test_hardening.js')
27+
.arguments('[file...]')
28+
.description(
29+
'Run the tests in test/harden directory. If no files are provided, all files within the directory will be run.'
30+
)
31+
.action(function(globs) {
32+
if (globs.length === 0) globs.push(path.join('test', 'harden', '*'));
33+
globs.forEach(function(glob) {
34+
syncGlob(glob).forEach(function(filename) {
35+
if (path.basename(filename)[0] === '_') return;
36+
console.log(process.argv[0], filename);
37+
execFileSync(process.argv[0], [filename], { stdio: 'inherit' });
38+
});
39+
});
40+
})
41+
.parse(process.argv);

src/setup_node_env/harden.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
var hook = require('require-in-the-middle');
21+
22+
hook(['child_process'], function(exports, name) {
23+
return require(`./patches/${name}`)(exports); // eslint-disable-line import/no-dynamic-require
24+
});

src/setup_node_env/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
* under the License.
1818
*/
1919

20+
require('./harden'); // this require MUST be executed before any others
2021
require('symbol-observable');
2122
require('./root');
2223
require('./node_version_validator');
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
// Ensure, when spawning a new child process, that the `options` and the
21+
// `options.env` object passed to the child process function doesn't inherit
22+
// from `Object.prototype`. This protects against similar RCE vulnerabilities
23+
// as described in CVE-2019-7609
24+
module.exports = function(cp) {
25+
// The `exec` function is currently just a wrapper around `execFile`. So for
26+
// now there's no need to patch it. If this changes in the future, our tests
27+
// will fail and we can uncomment the line below.
28+
//
29+
// cp.exec = new Proxy(cp.exec, { apply: patchOptions() });
30+
31+
cp.execFile = new Proxy(cp.execFile, { apply: patchOptions(true) });
32+
cp.fork = new Proxy(cp.fork, { apply: patchOptions(true) });
33+
cp.spawn = new Proxy(cp.spawn, { apply: patchOptions(true) });
34+
cp.execFileSync = new Proxy(cp.execFileSync, { apply: patchOptions(true) });
35+
cp.execSync = new Proxy(cp.execSync, { apply: patchOptions() });
36+
cp.spawnSync = new Proxy(cp.spawnSync, { apply: patchOptions(true) });
37+
38+
return cp;
39+
};
40+
41+
function patchOptions(hasArgs) {
42+
return function apply(target, thisArg, args) {
43+
var pos = 1;
44+
if (pos === args.length) {
45+
// fn(arg1)
46+
args[pos] = prototypelessSpawnOpts();
47+
} else if (pos < args.length) {
48+
if (hasArgs && (Array.isArray(args[pos]) || args[pos] == null)) {
49+
// fn(arg1, args, ...)
50+
pos++;
51+
}
52+
53+
if (typeof args[pos] === 'object' && args[pos] !== null) {
54+
// fn(arg1, {}, ...)
55+
// fn(arg1, args, {}, ...)
56+
args[pos] = prototypelessSpawnOpts(args[pos]);
57+
} else if (args[pos] == null) {
58+
// fn(arg1, null/undefined, ...)
59+
// fn(arg1, args, null/undefined, ...)
60+
args[pos] = prototypelessSpawnOpts();
61+
} else if (typeof args[pos] === 'function') {
62+
// fn(arg1, callback)
63+
// fn(arg1, args, callback)
64+
args.splice(pos, 0, prototypelessSpawnOpts());
65+
}
66+
}
67+
68+
return target.apply(thisArg, args);
69+
};
70+
}
71+
72+
function prototypelessSpawnOpts(obj) {
73+
var prototypelessObj = Object.assign(Object.create(null), obj);
74+
prototypelessObj.env = Object.assign(Object.create(null), prototypelessObj.env || process.env);
75+
return prototypelessObj;
76+
}

tasks/config/run.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,12 @@ module.exports = function(grunt) {
198198
args: ['scripts/notice', '--validate'],
199199
}),
200200

201+
test_hardening: scriptWithGithubChecks({
202+
title: 'Node.js hardening tests',
203+
cmd: NODE,
204+
args: ['scripts/test_hardening.js'],
205+
}),
206+
201207
apiIntegrationTests: scriptWithGithubChecks({
202208
title: 'API integration tests',
203209
cmd: NODE,

tasks/jenkins.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ module.exports = function(grunt) {
3636
'run:test_jest_integration',
3737
'run:test_projects',
3838
'run:test_karma_ci',
39+
'run:test_hardening',
3940
'run:apiIntegrationTests',
4041
]);
4142
};

test/harden/_echo.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#!/usr/bin/env sh
2+
3+
echo $POLLUTED$custom

test/harden/_fork.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Licensed to Elasticsearch B.V. under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch B.V. licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
console.log(`${process.env.POLLUTED || ''}${process.env.custom || ''}`);

0 commit comments

Comments
 (0)