@contrast/protect 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +12 -0
- package/README.md +9 -0
- package/lib/cli-rewriter.js +20 -0
- package/lib/error-handlers/constants.js +5 -0
- package/lib/error-handlers/index.js +13 -0
- package/lib/error-handlers/install/fastify3.js +88 -0
- package/lib/error-handlers/install/fastify3.test.js +142 -0
- package/lib/esm-loader.mjs +2 -0
- package/lib/esm-loader.test.mjs +11 -0
- package/lib/index.d.ts +36 -0
- package/lib/index.js +89 -0
- package/lib/index.test.js +32 -0
- package/lib/input-analysis/handlers.js +462 -0
- package/lib/input-analysis/handlers.test.js +898 -0
- package/lib/input-analysis/index.js +16 -0
- package/lib/input-analysis/index.test.js +28 -0
- package/lib/input-analysis/install/fastify3.js +79 -0
- package/lib/input-analysis/install/fastify3.test.js +71 -0
- package/lib/input-analysis/install/http.js +185 -0
- package/lib/input-analysis/install/http.test.js +315 -0
- package/lib/input-tracing/constants.js +5 -0
- package/lib/input-tracing/handlers/index.js +117 -0
- package/lib/input-tracing/handlers/index.test.js +395 -0
- package/lib/input-tracing/handlers/nosql-injection-mongo.js +48 -0
- package/lib/input-tracing/index.js +32 -0
- package/lib/input-tracing/install/README.md +1 -0
- package/lib/input-tracing/install/child-process.js +45 -0
- package/lib/input-tracing/install/child-process.test.js +112 -0
- package/lib/input-tracing/install/fs.js +107 -0
- package/lib/input-tracing/install/fs.test.js +118 -0
- package/lib/input-tracing/install/mysql.js +57 -0
- package/lib/input-tracing/install/mysql.test.js +108 -0
- package/lib/input-tracing/install/postgres.js +61 -0
- package/lib/input-tracing/install/postgres.test.js +125 -0
- package/lib/input-tracing/install/sequelize.js +51 -0
- package/lib/input-tracing/install/sequelize.test.js +79 -0
- package/lib/input-tracing/install/sqlite3.js +45 -0
- package/lib/input-tracing/install/sqlite3.test.js +88 -0
- package/lib/make-response-blocker.js +35 -0
- package/lib/make-response-blocker.test.js +88 -0
- package/lib/make-source-context.js +130 -0
- package/lib/make-source-context.test.js +298 -0
- package/lib/security-exception.js +12 -0
- package/lib/throw-security-exception.js +30 -0
- package/lib/throw-security-exception.test.js +50 -0
- package/lib/utils.js +88 -0
- package/lib/utils.test.js +40 -0
- package/package.json +32 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const sinon = require('sinon');
|
|
4
|
+
const { expect } = require('chai');
|
|
5
|
+
|
|
6
|
+
describe('protect input-tracing child-process', function() {
|
|
7
|
+
const cpInstr = require('./child-process');
|
|
8
|
+
const store = { protect: {} };
|
|
9
|
+
let cp, core;
|
|
10
|
+
|
|
11
|
+
beforeEach(function() {
|
|
12
|
+
const mocks = require('../../../../test/mocks/');
|
|
13
|
+
const patcher = require('@contrast/patcher');
|
|
14
|
+
|
|
15
|
+
cp = {
|
|
16
|
+
exec: sinon.stub(),
|
|
17
|
+
execSync: sinon.stub()
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
core = mocks.core();
|
|
21
|
+
core.logger = mocks.logger();
|
|
22
|
+
core.depHooks = mocks.depHooks();
|
|
23
|
+
core.depHooks.resolve.yields(cp);
|
|
24
|
+
core.scopes = mocks.scopes();
|
|
25
|
+
core.scopes.sources = {
|
|
26
|
+
getStore: sinon.stub().returns(store)
|
|
27
|
+
};
|
|
28
|
+
core.patcher = patcher(core);
|
|
29
|
+
core.protect = mocks.protect();
|
|
30
|
+
|
|
31
|
+
cpInstr(core);
|
|
32
|
+
|
|
33
|
+
core.protect.inputTracing.cpInstrumentation.install();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('handleCommandInjection() is called with expected values', function() {
|
|
37
|
+
const methodArgs = ['foo', 'bar'];
|
|
38
|
+
|
|
39
|
+
['exec', 'execSync'].forEach((method) => {
|
|
40
|
+
const name = `child_process.${method}`;
|
|
41
|
+
|
|
42
|
+
it(`${name}`, function() {
|
|
43
|
+
cp[method](...methodArgs);
|
|
44
|
+
|
|
45
|
+
const value = methodArgs[0];
|
|
46
|
+
|
|
47
|
+
expect(core.captureStacktrace).to.have.been.calledOnceWith(
|
|
48
|
+
{ name, value },
|
|
49
|
+
{ constructorOpt: cp[method] }
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(core.protect.inputTracing.handleCommandInjection).to.have.been.calledWith(
|
|
53
|
+
store.protect,
|
|
54
|
+
{ name, value }
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('handleCommandInjection() is not called when method args are not relevant', function() {
|
|
61
|
+
const methodArgs = [1, undefined];
|
|
62
|
+
|
|
63
|
+
['exec', 'execSync'].forEach((method) => {
|
|
64
|
+
const name = `child_process.${method}`;
|
|
65
|
+
|
|
66
|
+
it(name, function() {
|
|
67
|
+
cp[method](...methodArgs);
|
|
68
|
+
|
|
69
|
+
expect(core.captureStacktrace).to.not.have.been.called;
|
|
70
|
+
expect(core.protect.inputTracing.handleCommandInjection).to.not.have.been.called;
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('handleCommandInjection() is not called when instrumentation is locked', function() {
|
|
76
|
+
const methodArgs = ['foo', 'bar'];
|
|
77
|
+
|
|
78
|
+
beforeEach(function() {
|
|
79
|
+
core.scopes.instrumentation.isLocked.returns(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
['exec', 'execSync'].forEach((method) => {
|
|
83
|
+
const name = `child_process.${method}`;
|
|
84
|
+
|
|
85
|
+
it(name, function() {
|
|
86
|
+
cp[method](...methodArgs);
|
|
87
|
+
|
|
88
|
+
expect(core.captureStacktrace).to.not.have.been.called;
|
|
89
|
+
expect(core.protect.inputTracing.handleCommandInjection).to.not.have.been.called;
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('handleCommandInjection() is not called when there is no Protect sourceContext', function() {
|
|
95
|
+
const methodArgs = ['foo', 'bar'];
|
|
96
|
+
|
|
97
|
+
beforeEach(function() {
|
|
98
|
+
core.scopes.sources.getStore.returns({ protect: null });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
['exec', 'execSync'].forEach((method) => {
|
|
102
|
+
const name = `child_process.${method}`;
|
|
103
|
+
|
|
104
|
+
it(name, function() {
|
|
105
|
+
cp[method](...methodArgs);
|
|
106
|
+
|
|
107
|
+
expect(core.captureStacktrace).to.not.have.been.called;
|
|
108
|
+
expect(core.protect.inputTracing.handleCommandInjection).to.not.have.been.called;
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isString } = require('@contrast/common');
|
|
4
|
+
const { patchType } = require('../constants');
|
|
5
|
+
|
|
6
|
+
const fsMethods = [
|
|
7
|
+
{ method: 'access' },
|
|
8
|
+
{ method: 'accessSync' },
|
|
9
|
+
{ method: 'appendFile' },
|
|
10
|
+
{ method: 'appendFileSync' },
|
|
11
|
+
{ method: 'chmod' },
|
|
12
|
+
{ method: 'chmodSync' },
|
|
13
|
+
{ method: 'chown' },
|
|
14
|
+
{ method: 'chownSync' },
|
|
15
|
+
{ method: 'copyFile', indices: [0, 1] },
|
|
16
|
+
{ method: 'copyFileSync', indices: [0, 1] },
|
|
17
|
+
{ method: 'createReadStream' },
|
|
18
|
+
{ method: 'createWriteStream' },
|
|
19
|
+
{ method: 'lchmod' },
|
|
20
|
+
{ method: 'lchmodSync' },
|
|
21
|
+
{ method: 'lchown' },
|
|
22
|
+
{ method: 'lchownSync' },
|
|
23
|
+
{ method: 'mkdir' },
|
|
24
|
+
{ method: 'mkdirSync' },
|
|
25
|
+
{ method: 'open' },
|
|
26
|
+
{ method: 'openSync' },
|
|
27
|
+
{ method: 'readFile' },
|
|
28
|
+
{ method: 'readFileSync' },
|
|
29
|
+
{ method: 'readdir' },
|
|
30
|
+
{ method: 'readdirSync' },
|
|
31
|
+
{ method: 'readlink' },
|
|
32
|
+
{ method: 'readlinkSync' },
|
|
33
|
+
{ method: 'rename', indices: [0, 1] },
|
|
34
|
+
{ method: 'renameSync', indices: [0, 1] },
|
|
35
|
+
{ method: 'rmdir' },
|
|
36
|
+
{ method: 'rmdirSync' },
|
|
37
|
+
{ method: 'symlink', indices: [0, 1] },
|
|
38
|
+
{ method: 'symlinkSync', indices: [0, 1] },
|
|
39
|
+
{ method: 'truncate' },
|
|
40
|
+
{ method: 'truncateSync' },
|
|
41
|
+
{ method: 'unlink' },
|
|
42
|
+
{ method: 'unlinkSync' },
|
|
43
|
+
{ method: 'writeFile' },
|
|
44
|
+
{ method: 'writeFileSync' },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
module.exports = function(core) {
|
|
48
|
+
const {
|
|
49
|
+
scopes: { sources, instrumentation },
|
|
50
|
+
patcher,
|
|
51
|
+
depHooks,
|
|
52
|
+
captureStacktrace,
|
|
53
|
+
protect: { inputTracing }
|
|
54
|
+
} = core;
|
|
55
|
+
|
|
56
|
+
function getValues(indices, args) {
|
|
57
|
+
return indices.reduce((acc, idx) => {
|
|
58
|
+
const value = args[idx];
|
|
59
|
+
if (value && isString(value)) acc.push(value);
|
|
60
|
+
return acc;
|
|
61
|
+
}, []);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function install() {
|
|
65
|
+
depHooks.resolve({ name: 'fs' }, fs => {
|
|
66
|
+
fsMethods.forEach(({ method, indices = [0] }) => {
|
|
67
|
+
const name = `fs.${method}`;
|
|
68
|
+
patcher.patch(fs, method, {
|
|
69
|
+
name,
|
|
70
|
+
patchType,
|
|
71
|
+
pre({ args, hooked, name }) {
|
|
72
|
+
// don't proceed if instrumentation is off e.g. within require() call
|
|
73
|
+
if (instrumentation.isLocked()) return;
|
|
74
|
+
|
|
75
|
+
// obtain the Protect sourceContext
|
|
76
|
+
const sourceContext = sources.getStore()?.protect;
|
|
77
|
+
if (!sourceContext) return;
|
|
78
|
+
|
|
79
|
+
// build list of values on which to perform INPUT TRACING
|
|
80
|
+
const values = getValues(indices, args);
|
|
81
|
+
if (!values.length) return;
|
|
82
|
+
|
|
83
|
+
// while we need to check whether instrumentation is locked above, we
|
|
84
|
+
// don't need to necessarily need to lock it here - there are no
|
|
85
|
+
// lower-level calls that we instrument
|
|
86
|
+
for (const value of values) {
|
|
87
|
+
const sinkContext = captureStacktrace(
|
|
88
|
+
{ name, value },
|
|
89
|
+
{ constructorOpt: hooked }
|
|
90
|
+
);
|
|
91
|
+
inputTracing.handlePathTraversal(sourceContext, sinkContext);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const fsInstrumentation = inputTracing.fsInstrumentation = {
|
|
100
|
+
getValues,
|
|
101
|
+
install,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
return fsInstrumentation;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
module.exports.fsMethods = fsMethods;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const sinon = require('sinon');
|
|
4
|
+
const { expect } = require('chai');
|
|
5
|
+
|
|
6
|
+
describe('protect input-tracing fs', function() {
|
|
7
|
+
const fsInstr = require('./fs');
|
|
8
|
+
const store = { protect: {} };
|
|
9
|
+
let fs;
|
|
10
|
+
let core;
|
|
11
|
+
|
|
12
|
+
function makeFsMock() {
|
|
13
|
+
return fsInstr.fsMethods.reduce((fs, { method, indices }) => {
|
|
14
|
+
fs[method] = sinon.stub();
|
|
15
|
+
return fs;
|
|
16
|
+
}, {});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
beforeEach(function() {
|
|
20
|
+
const mocks = require('../../../../test/mocks');
|
|
21
|
+
const patcher = require('@contrast/patcher');
|
|
22
|
+
|
|
23
|
+
fs = makeFsMock();
|
|
24
|
+
|
|
25
|
+
core = mocks.core();
|
|
26
|
+
core.logger = mocks.logger();
|
|
27
|
+
core.depHooks = mocks.depHooks();
|
|
28
|
+
core.depHooks.resolve.yields(fs);
|
|
29
|
+
core.scopes = mocks.scopes();
|
|
30
|
+
sinon.stub(core.scopes.sources, 'getStore').returns(store);
|
|
31
|
+
core.patcher = patcher(core);
|
|
32
|
+
core.protect = mocks.protect();
|
|
33
|
+
|
|
34
|
+
fsInstr(core);
|
|
35
|
+
|
|
36
|
+
core.protect.inputTracing.fsInstrumentation.install();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('handlePathTraversal() is called with expected values', function() {
|
|
40
|
+
const methodArgs = ['foo', 'bar'];
|
|
41
|
+
|
|
42
|
+
for (const { method, indices = [0] } of fsInstr.fsMethods) {
|
|
43
|
+
const name = `fs.${method}`;
|
|
44
|
+
|
|
45
|
+
it(`${name}`, function() {
|
|
46
|
+
fs[method](...methodArgs);
|
|
47
|
+
|
|
48
|
+
const calledWith = indices.length === 0 ? 'calledOnceWith' : 'calledWith';
|
|
49
|
+
for (const idx of indices) {
|
|
50
|
+
|
|
51
|
+
const value = methodArgs[idx];
|
|
52
|
+
|
|
53
|
+
// this is profound - we are asserting that the stacktrace constructor
|
|
54
|
+
// opt is the original method
|
|
55
|
+
expect(core.captureStacktrace).to.have.been[calledWith](
|
|
56
|
+
{ name, value },
|
|
57
|
+
{ constructorOpt: fs[method] }
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
expect(core.protect.inputTracing.handlePathTraversal).to.have.been.calledWith(
|
|
61
|
+
store.protect,
|
|
62
|
+
{ name, value }
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('handlePathTraversal() is not called when method args are not relevant', function() {
|
|
70
|
+
const methodArgs = [1, undefined];
|
|
71
|
+
|
|
72
|
+
for (const { method } of fsInstr.fsMethods) {
|
|
73
|
+
const name = `fs.${method}`;
|
|
74
|
+
|
|
75
|
+
it(name, function() {
|
|
76
|
+
fs[method](...methodArgs);
|
|
77
|
+
|
|
78
|
+
expect(core.protect.inputTracing.handlePathTraversal).not.to.have.been.called;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('handlePathTraversal() is not called when instrumentation is locked', function() {
|
|
84
|
+
const methodArgs = ['foo', 'bar'];
|
|
85
|
+
|
|
86
|
+
beforeEach(function() {
|
|
87
|
+
core.scopes.instrumentation.isLocked.returns(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
for (const { method } of fsInstr.fsMethods) {
|
|
91
|
+
const name = `fs.${method}`;
|
|
92
|
+
|
|
93
|
+
it(name, function() {
|
|
94
|
+
fs[method](...methodArgs);
|
|
95
|
+
|
|
96
|
+
expect(core.protect.inputTracing.handlePathTraversal).not.to.have.been.called;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('handlePathTraversal() is not called when there is no Protect sourceContext', function() {
|
|
102
|
+
const methodArgs = ['foo', 'bar'];
|
|
103
|
+
|
|
104
|
+
beforeEach(function() {
|
|
105
|
+
core.scopes.sources.getStore.returns({ protect: null });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
for (const { method } of fsInstr.fsMethods) {
|
|
109
|
+
const name = `fs.${method}`;
|
|
110
|
+
|
|
111
|
+
it(name, function() {
|
|
112
|
+
fs[method](...methodArgs);
|
|
113
|
+
|
|
114
|
+
expect(core.protect.inputTracing.handlePathTraversal).not.to.have.been.called;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isString } = require('@contrast/common');
|
|
4
|
+
const { patchType } = require('../constants');
|
|
5
|
+
|
|
6
|
+
module.exports = function(core) {
|
|
7
|
+
const {
|
|
8
|
+
depHooks,
|
|
9
|
+
patcher,
|
|
10
|
+
captureStacktrace,
|
|
11
|
+
scopes: { sources },
|
|
12
|
+
protect: { inputTracing }
|
|
13
|
+
} = core;
|
|
14
|
+
|
|
15
|
+
const mysqlInstr = {};
|
|
16
|
+
|
|
17
|
+
mysqlInstr.getValueFromArgs = function([value]) {
|
|
18
|
+
if (isString(value)) {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
mysqlInstr.install = function() {
|
|
24
|
+
[
|
|
25
|
+
{ module: 'mysql', file: 'lib/Connection.js', method: 'query' },
|
|
26
|
+
{ module: 'mysql2', file: 'lib/Connection.js', method: 'execute' }
|
|
27
|
+
].forEach(
|
|
28
|
+
({ module, file, method }) => {
|
|
29
|
+
depHooks.resolve({ module, file, method }, conn => {
|
|
30
|
+
const name = `${module}.Connection.prototype.${method}`;
|
|
31
|
+
|
|
32
|
+
patcher.patch(conn.prototype, method, {
|
|
33
|
+
name,
|
|
34
|
+
patchType,
|
|
35
|
+
pre({ args, hooked, name }) {
|
|
36
|
+
const sourceContext = sources.getStore()?.protect;
|
|
37
|
+
if (!sourceContext) return;
|
|
38
|
+
|
|
39
|
+
const value = mysqlInstr.getValueFromArgs(args);
|
|
40
|
+
if (!value) return;
|
|
41
|
+
|
|
42
|
+
const sinkContext = captureStacktrace(
|
|
43
|
+
{ name, value },
|
|
44
|
+
{ constructorOpt: hooked }
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
inputTracing.handleSqlInjection(sourceContext, sinkContext);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
core.protect.inputTracing.mysqlInstrumentation = mysqlInstr;
|
|
55
|
+
|
|
56
|
+
return mysqlInstr;
|
|
57
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { expect } = require('chai');
|
|
4
|
+
const sinon = require('sinon');
|
|
5
|
+
|
|
6
|
+
describe('protect input-tracing installs: mysql interfaces', function() {
|
|
7
|
+
const store = { protect: {} };
|
|
8
|
+
|
|
9
|
+
let core;
|
|
10
|
+
let inputTracing;
|
|
11
|
+
|
|
12
|
+
beforeEach(function() {
|
|
13
|
+
const mocks = require('../../../../test/mocks');
|
|
14
|
+
const patcher = require('@contrast/patcher');
|
|
15
|
+
|
|
16
|
+
core = mocks.core();
|
|
17
|
+
core.logger = mocks.logger();
|
|
18
|
+
core.patcher = patcher(core);
|
|
19
|
+
core.scopes = mocks.scopes();
|
|
20
|
+
core.depHooks = mocks.depHooks();
|
|
21
|
+
core.protect = mocks.protect();
|
|
22
|
+
({ protect: { inputTracing } } = core);
|
|
23
|
+
|
|
24
|
+
sinon.stub(core.scopes.sources, 'getStore').returns(store);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('mysql', function() {
|
|
28
|
+
let Connection;
|
|
29
|
+
|
|
30
|
+
beforeEach(function() {
|
|
31
|
+
Connection = function() {};
|
|
32
|
+
Connection.prototype.query = sinon.stub();
|
|
33
|
+
core.depHooks.resolve.yields(Connection);
|
|
34
|
+
require('./mysql')(core).install();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('instruments connection.query()', function() {
|
|
38
|
+
it('handleSqlInjection() is called with valid expected values', function() {
|
|
39
|
+
const conn = new Connection();
|
|
40
|
+
const value = 'SELECT "foo"';
|
|
41
|
+
conn.query(value);
|
|
42
|
+
|
|
43
|
+
expect(inputTracing.handleSqlInjection).to.have.been.calledWith(
|
|
44
|
+
{},
|
|
45
|
+
{ name: 'mysql.Connection.prototype.query', value }
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('handleSqlInjection() is not called if there is no sourceContext', function() {
|
|
50
|
+
core.scopes.sources.getStore.returns({ protect: undefined });
|
|
51
|
+
const conn = new Connection();
|
|
52
|
+
const value = 'SELECT "foo"';
|
|
53
|
+
conn.query(value);
|
|
54
|
+
|
|
55
|
+
expect(inputTracing.handleSqlInjection).to.have.not.been.called;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('handleSqlInjection() is not called if the sql value is empty or not a string', function() {
|
|
59
|
+
const conn = new Connection();
|
|
60
|
+
conn.query('');
|
|
61
|
+
conn.query(100);
|
|
62
|
+
|
|
63
|
+
expect(inputTracing.handleSqlInjection).to.have.not.been.called;
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('mysql2', function() {
|
|
69
|
+
let Connection;
|
|
70
|
+
|
|
71
|
+
beforeEach(function() {
|
|
72
|
+
Connection = function() {};
|
|
73
|
+
Connection.prototype.execute = sinon.stub();
|
|
74
|
+
core.depHooks.resolve.yields(Connection);
|
|
75
|
+
require('./mysql')(core).install();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('instruments connection.execute()', function() {
|
|
79
|
+
it('handleSqlInjection() is called with expected values', function() {
|
|
80
|
+
const conn = new Connection();
|
|
81
|
+
const value = 'SELECT "foo"';
|
|
82
|
+
conn.execute(value);
|
|
83
|
+
|
|
84
|
+
expect(inputTracing.handleSqlInjection).to.have.been.calledWith(
|
|
85
|
+
{},
|
|
86
|
+
{ name: 'mysql2.Connection.prototype.execute', value }
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('handleSqlInjection() is not called if there is no sourceContext', function() {
|
|
91
|
+
core.scopes.sources.getStore.returns({ protect: undefined });
|
|
92
|
+
const conn = new Connection();
|
|
93
|
+
const value = 'SELECT "foo"';
|
|
94
|
+
conn.execute(value);
|
|
95
|
+
|
|
96
|
+
expect(inputTracing.handleSqlInjection).to.have.not.been.called;
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('handleSqlInjection() is not called if the sql value is empty or not a string', function() {
|
|
100
|
+
const conn = new Connection();
|
|
101
|
+
conn.execute('');
|
|
102
|
+
conn.execute(100);
|
|
103
|
+
|
|
104
|
+
expect(inputTracing.handleSqlInjection).to.have.not.been.called;
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isString } = require('@contrast/common');
|
|
4
|
+
const { patchType } = require('../constants');
|
|
5
|
+
|
|
6
|
+
module.exports = function(core) {
|
|
7
|
+
const {
|
|
8
|
+
depHooks,
|
|
9
|
+
patcher,
|
|
10
|
+
scopes: { sources },
|
|
11
|
+
captureStacktrace,
|
|
12
|
+
protect: { inputTracing }
|
|
13
|
+
} = core;
|
|
14
|
+
|
|
15
|
+
function getQueryFromArgs([value]) {
|
|
16
|
+
const query = value?.text || value;
|
|
17
|
+
if (query && isString(query)) return query;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function preHook({ args, hooked, name }) {
|
|
21
|
+
const sourceContext = sources.getStore()?.protect;
|
|
22
|
+
if (!sourceContext) return;
|
|
23
|
+
|
|
24
|
+
const value = getQueryFromArgs(args);
|
|
25
|
+
if (!value) return;
|
|
26
|
+
|
|
27
|
+
const sinkContext = captureStacktrace(
|
|
28
|
+
{ name, value },
|
|
29
|
+
{ constructorOpt: hooked }
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
inputTracing.handleSqlInjection(sourceContext, sinkContext);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function install() {
|
|
36
|
+
depHooks.resolve({ name: 'pg', file: 'lib/client.js' }, client => {
|
|
37
|
+
const name = 'pg.Client.prototype.query';
|
|
38
|
+
patcher.patch(client.prototype, 'query', {
|
|
39
|
+
name,
|
|
40
|
+
patchType,
|
|
41
|
+
pre: preHook
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
depHooks.resolve({ name: 'pg-pool' }, pool => {
|
|
46
|
+
const name = 'pg-pool.Pool.prototype.query';
|
|
47
|
+
patcher.patch(pool.prototype, 'query', {
|
|
48
|
+
name,
|
|
49
|
+
patchType,
|
|
50
|
+
pre: preHook
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const postgresInstr = core.protect.inputTracing.postgresInstrumentation = {
|
|
56
|
+
getQueryFromArgs,
|
|
57
|
+
install
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return postgresInstr;
|
|
61
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { expect } = require('chai');
|
|
4
|
+
const sinon = require('sinon');
|
|
5
|
+
|
|
6
|
+
describe('protect input-tracing installs: postgres interfaces', function() {
|
|
7
|
+
const sourceContext = {};
|
|
8
|
+
const store = { protect: sourceContext };
|
|
9
|
+
const query = 'SELECT "foo"';
|
|
10
|
+
|
|
11
|
+
let core;
|
|
12
|
+
let inputTracing;
|
|
13
|
+
|
|
14
|
+
beforeEach(function() {
|
|
15
|
+
const mocks = require('../../../../test/mocks');
|
|
16
|
+
const patcher = require('@contrast/patcher');
|
|
17
|
+
|
|
18
|
+
core = mocks.core();
|
|
19
|
+
core.logger = mocks.logger();
|
|
20
|
+
core.patcher = patcher(core);
|
|
21
|
+
core.scopes = mocks.scopes();
|
|
22
|
+
sinon.stub(core.scopes.sources, 'getStore').returns(store);
|
|
23
|
+
core.depHooks = mocks.depHooks();
|
|
24
|
+
core.protect = mocks.protect();
|
|
25
|
+
({ protect: { inputTracing } } = core);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('pg.Client.prototype.query', function() {
|
|
29
|
+
let Client;
|
|
30
|
+
|
|
31
|
+
beforeEach(function() {
|
|
32
|
+
Client = function() {};
|
|
33
|
+
Client.prototype.query = sinon.stub();
|
|
34
|
+
core
|
|
35
|
+
.depHooks
|
|
36
|
+
.resolve
|
|
37
|
+
.withArgs({ name: 'pg', file: 'lib/client.js' })
|
|
38
|
+
.yields(Client);
|
|
39
|
+
require('./postgres')(core).install();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('handleSqlInjection() is called with expected values', function() {
|
|
43
|
+
const conn = new Client();
|
|
44
|
+
conn.query(query);
|
|
45
|
+
conn.query({ text: query });
|
|
46
|
+
|
|
47
|
+
const sinkContext = { name: 'pg.Client.prototype.query', value: query };
|
|
48
|
+
|
|
49
|
+
expect(inputTracing.handleSqlInjection.callCount).to.eql(2);
|
|
50
|
+
[0, 1].forEach(callNum => {
|
|
51
|
+
expect(inputTracing.handleSqlInjection.getCall(callNum).calledWithExactly(store, sinkContext));
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('handleSqlInjection() is not called if there is no sourceContext', function() {
|
|
56
|
+
core.scopes.sources.getStore.returns({ protect: undefined });
|
|
57
|
+
|
|
58
|
+
const conn = new Client();
|
|
59
|
+
conn.query(query);
|
|
60
|
+
conn.query({ text: query });
|
|
61
|
+
|
|
62
|
+
expect(inputTracing.handleSqlInjection).to.have.not.been.called;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('handleSqlInjection() is not called if the sql value is empty or not a string', function() {
|
|
66
|
+
const conn = new Client();
|
|
67
|
+
conn.query('');
|
|
68
|
+
conn.query(100);
|
|
69
|
+
conn.query({ text: '' });
|
|
70
|
+
conn.query({ text: 100 });
|
|
71
|
+
|
|
72
|
+
expect(inputTracing.handleSqlInjection).to.have.not.been.called;
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('pg-pool.Pool.prototype.query', function() {
|
|
77
|
+
let Pool;
|
|
78
|
+
|
|
79
|
+
beforeEach(function() {
|
|
80
|
+
Pool = function() {};
|
|
81
|
+
Pool.prototype.query = sinon.stub();
|
|
82
|
+
core
|
|
83
|
+
.depHooks
|
|
84
|
+
.resolve
|
|
85
|
+
.withArgs({ name: 'pg-pool' })
|
|
86
|
+
.yields(Pool);
|
|
87
|
+
|
|
88
|
+
require('./postgres')(core).install();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('handleSqlInjection() is called with expected values', function() {
|
|
92
|
+
const pool = new Pool();
|
|
93
|
+
pool.query(query);
|
|
94
|
+
pool.query({ text: query });
|
|
95
|
+
|
|
96
|
+
const sinkContext = { name: 'pg-pool.Pool.prototype.query', value: query };
|
|
97
|
+
|
|
98
|
+
expect(inputTracing.handleSqlInjection.callCount).to.eql(2);
|
|
99
|
+
[0, 1].forEach(callNum => {
|
|
100
|
+
expect(inputTracing.handleSqlInjection.getCall(callNum).calledWithExactly(store, sinkContext));
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('handleSqlInjection() is not called if there is no sourceContext', function() {
|
|
105
|
+
core.scopes.sources.getStore.returns({ protect: undefined });
|
|
106
|
+
|
|
107
|
+
const pool = new Pool();
|
|
108
|
+
const value = 'SELECT "foo"';
|
|
109
|
+
pool.query(value);
|
|
110
|
+
pool.query({ text: value });
|
|
111
|
+
|
|
112
|
+
expect(inputTracing.handleSqlInjection).to.have.not.been.called;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('handleSqlInjection() is not called if the sql value is empty or not a string', function() {
|
|
116
|
+
const pool = new Pool();
|
|
117
|
+
pool.query('');
|
|
118
|
+
pool.query(100);
|
|
119
|
+
pool.query({ text: '' });
|
|
120
|
+
pool.query({ text: 100 });
|
|
121
|
+
|
|
122
|
+
expect(inputTracing.handleSqlInjection).to.have.not.been.called;
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|