Skip to content

Commit 2feabe6

Browse files
committed
fix: security and reliability improvements for maintainership
- Fix runString() using tmpdir reference instead of tmpdir() call (fixes #320) - Replace exec() with execFile() to prevent command injection in checkSyntaxFile, getVersion, and getVersionSync - Add temp file cleanup in runString() and checkSyntax() via .finally() - Replace custom extend() with Object.assign - Re-enable getVersion/getVersionSync tests (were disabled since #158) - Add GitHub Actions CI matrix (Node 18/20/22, Python 3.10/3.12, 3 OSes) - Update minimum Node.js engine from 0.10 to 16
1 parent 0cde240 commit 2feabe6

File tree

5 files changed

+64
-63
lines changed

5 files changed

+64
-63
lines changed

.github/workflows/ci.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [master]
6+
pull_request:
7+
branches: [master]
8+
9+
jobs:
10+
test:
11+
runs-on: ${{ matrix.os }}
12+
strategy:
13+
matrix:
14+
os: [ubuntu-latest, macos-latest, windows-latest]
15+
node-version: [18, 20, 22]
16+
python-version: ['3.10', '3.12']
17+
steps:
18+
- uses: actions/checkout@v4
19+
- uses: actions/setup-node@v4
20+
with:
21+
node-version: ${{ matrix.node-version }}
22+
- uses: actions/setup-python@v5
23+
with:
24+
python-version: ${{ matrix.python-version }}
25+
- run: npm install
26+
- run: npm test

index.ts

Lines changed: 19 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import {
33
ChildProcess,
44
spawn,
55
SpawnOptions,
6-
exec,
7-
execSync,
6+
execFile,
7+
execFileSync,
88
} from 'child_process';
99
import { EOL as newline, tmpdir } from 'os';
1010
import { join, sep } from 'path';
1111
import { Readable, Transform, TransformCallback, Writable } from 'stream';
12-
import { writeFile, writeFileSync } from 'fs';
12+
import { writeFile, writeFileSync, unlink } from 'fs';
1313
import { promisify } from 'util';
1414

1515
function toArray<T>(source?: T | T[]): T[] {
@@ -21,28 +21,8 @@ function toArray<T>(source?: T | T[]): T[] {
2121
return source;
2222
}
2323

24-
/**
25-
* adds arguments as properties to obj
26-
*/
27-
function extend(obj: {}, ...args) {
28-
Array.prototype.slice.call(arguments, 1).forEach(function (source) {
29-
if (source) {
30-
for (let key in source) {
31-
obj[key] = source[key];
32-
}
33-
}
34-
});
35-
return obj;
36-
}
37-
38-
/**
39-
* gets a random int from 0-10000000000
40-
*/
41-
function getRandomInt() {
42-
return Math.floor(Math.random() * 10000000000);
43-
}
44-
45-
const execPromise = promisify(exec);
24+
const execFilePromise = promisify(execFile);
25+
const unlinkPromise = promisify(unlink);
4626

4727
export interface Options extends SpawnOptions {
4828
/**
@@ -171,7 +151,7 @@ export class PythonShell extends EventEmitter {
171151
let errorData = '';
172152
EventEmitter.call(this);
173153

174-
options = <Options>extend({}, PythonShell.defaultOptions, options);
154+
options = <Options>Object.assign({}, PythonShell.defaultOptions, options);
175155
let pythonPath: string;
176156
if (!options.pythonPath) {
177157
pythonPath = PythonShell.defaultPythonPath;
@@ -266,7 +246,7 @@ export class PythonShell extends EventEmitter {
266246
'process exited with code ' + self.exitCode,
267247
);
268248
}
269-
err = <PythonShellError>extend(err, {
249+
Object.assign(err, {
270250
executable: pythonPath,
271251
options: pythonOptions.length ? pythonOptions : null,
272252
script: self.scriptPath,
@@ -313,12 +293,14 @@ export class PythonShell extends EventEmitter {
313293
* @returns rejects promise w/ string error output if syntax failure
314294
*/
315295
static async checkSyntax(code: string) {
316-
const randomInt = getRandomInt();
317-
const filePath = tmpdir() + sep + `pythonShellSyntaxCheck${randomInt}.py`;
296+
const filePath =
297+
tmpdir() + sep + `pythonShellSyntaxCheck${Date.now()}.py`;
318298

319299
const writeFilePromise = promisify(writeFile);
320300
return writeFilePromise(filePath, code).then(() => {
321-
return this.checkSyntaxFile(filePath);
301+
return this.checkSyntaxFile(filePath).finally(() => {
302+
unlinkPromise(filePath).catch(() => {});
303+
});
322304
});
323305
}
324306

@@ -334,8 +316,7 @@ export class PythonShell extends EventEmitter {
334316
*/
335317
static async checkSyntaxFile(filePath: string) {
336318
const pythonPath = this.getPythonPath();
337-
let compileCommand = `${pythonPath} -m py_compile ${filePath}`;
338-
return execPromise(compileCommand);
319+
return execFilePromise(pythonPath, ['-m', 'py_compile', filePath]);
339320
}
340321

341322
/**
@@ -370,21 +351,22 @@ export class PythonShell extends EventEmitter {
370351
*/
371352
static runString(code: string, options?: Options) {
372353
// put code in temp file
373-
const randomInt = getRandomInt();
374-
const filePath = tmpdir + sep + `pythonShellFile${randomInt}.py`;
354+
const filePath = tmpdir() + sep + `pythonShellFile${Date.now()}.py`;
375355
writeFileSync(filePath, code);
376356

377-
return PythonShell.run(filePath, options);
357+
return PythonShell.run(filePath, options).finally(() => {
358+
unlinkPromise(filePath).catch(() => {});
359+
});
378360
}
379361

380362
static getVersion(pythonPath?: string) {
381363
if (!pythonPath) pythonPath = this.getPythonPath();
382-
return execPromise(pythonPath + ' --version');
364+
return execFilePromise(pythonPath, ['--version']);
383365
}
384366

385367
static getVersionSync(pythonPath?: string) {
386368
if (!pythonPath) pythonPath = this.getPythonPath();
387-
return execSync(pythonPath + ' --version').toString();
369+
return execFileSync(pythonPath, ['--version']).toString();
388370
}
389371

390372
/**

package-lock.json

Lines changed: 1 addition & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"email": "nicolas@extrabacon.net"
4141
},
4242
"engines": {
43-
"node": ">=0.10"
43+
"node": ">=16"
4444
},
4545
"license": "MIT"
4646
}

test/test-python-shell.ts

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -98,27 +98,24 @@ describe('PythonShell', function () {
9898
});
9999
});
100100

101-
// #158 these tests are failing on appveyor windows node 8/10 python 2/3
102-
// but they work locally on my windows machine .....
103-
// these methods are not that important so just commenting out tests untill someone fixes them
104-
// describe("#getVersion", function(){
105-
// it('should return a string', function(done){
106-
// PythonShell.getVersion().then((out)=>{
107-
// const version = out.stdout
108-
// version.should.be.a.String();
109-
// version.length.should.be.greaterThan(0)
110-
// done()
111-
// })
112-
// })
113-
// })
101+
describe('#getVersion', function () {
102+
it('should return a string', function (done) {
103+
PythonShell.getVersion().then((out) => {
104+
const version = out.stdout;
105+
version.should.be.a.String();
106+
version.length.should.be.greaterThan(0);
107+
done();
108+
});
109+
});
110+
});
114111

115-
// describe("#getVersionSync", function(){
116-
// it('should return a string', function(){
117-
// const version = PythonShell.getVersionSync()
118-
// version.should.be.a.String();
119-
// version.length.should.be.greaterThan(0)
120-
// })
121-
// })
112+
describe('#getVersionSync', function () {
113+
it('should return a string', function () {
114+
const version = PythonShell.getVersionSync();
115+
version.should.be.a.String();
116+
version.length.should.be.greaterThan(0);
117+
});
118+
});
122119

123120
describe('#runString(script, options)', function () {
124121
before(() => {

0 commit comments

Comments
 (0)