@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.
Files changed (48) hide show
  1. package/LICENSE +12 -0
  2. package/README.md +9 -0
  3. package/lib/cli-rewriter.js +20 -0
  4. package/lib/error-handlers/constants.js +5 -0
  5. package/lib/error-handlers/index.js +13 -0
  6. package/lib/error-handlers/install/fastify3.js +88 -0
  7. package/lib/error-handlers/install/fastify3.test.js +142 -0
  8. package/lib/esm-loader.mjs +2 -0
  9. package/lib/esm-loader.test.mjs +11 -0
  10. package/lib/index.d.ts +36 -0
  11. package/lib/index.js +89 -0
  12. package/lib/index.test.js +32 -0
  13. package/lib/input-analysis/handlers.js +462 -0
  14. package/lib/input-analysis/handlers.test.js +898 -0
  15. package/lib/input-analysis/index.js +16 -0
  16. package/lib/input-analysis/index.test.js +28 -0
  17. package/lib/input-analysis/install/fastify3.js +79 -0
  18. package/lib/input-analysis/install/fastify3.test.js +71 -0
  19. package/lib/input-analysis/install/http.js +185 -0
  20. package/lib/input-analysis/install/http.test.js +315 -0
  21. package/lib/input-tracing/constants.js +5 -0
  22. package/lib/input-tracing/handlers/index.js +117 -0
  23. package/lib/input-tracing/handlers/index.test.js +395 -0
  24. package/lib/input-tracing/handlers/nosql-injection-mongo.js +48 -0
  25. package/lib/input-tracing/index.js +32 -0
  26. package/lib/input-tracing/install/README.md +1 -0
  27. package/lib/input-tracing/install/child-process.js +45 -0
  28. package/lib/input-tracing/install/child-process.test.js +112 -0
  29. package/lib/input-tracing/install/fs.js +107 -0
  30. package/lib/input-tracing/install/fs.test.js +118 -0
  31. package/lib/input-tracing/install/mysql.js +57 -0
  32. package/lib/input-tracing/install/mysql.test.js +108 -0
  33. package/lib/input-tracing/install/postgres.js +61 -0
  34. package/lib/input-tracing/install/postgres.test.js +125 -0
  35. package/lib/input-tracing/install/sequelize.js +51 -0
  36. package/lib/input-tracing/install/sequelize.test.js +79 -0
  37. package/lib/input-tracing/install/sqlite3.js +45 -0
  38. package/lib/input-tracing/install/sqlite3.test.js +88 -0
  39. package/lib/make-response-blocker.js +35 -0
  40. package/lib/make-response-blocker.test.js +88 -0
  41. package/lib/make-source-context.js +130 -0
  42. package/lib/make-source-context.test.js +298 -0
  43. package/lib/security-exception.js +12 -0
  44. package/lib/throw-security-exception.js +30 -0
  45. package/lib/throw-security-exception.test.js +50 -0
  46. package/lib/utils.js +88 -0
  47. package/lib/utils.test.js +40 -0
  48. package/package.json +32 -0
@@ -0,0 +1,117 @@
1
+ 'use strict';
2
+
3
+ module.exports = function(core) {
4
+ const { protect: { agentLib, inputTracing, throwSecurityException } } = core;
5
+
6
+ /**
7
+ * Util for pulling results from context for the particular rule id
8
+ * @param {string} ruleId rule id
9
+ * @param {object} context async storage data for protect
10
+ * @returns {AnalysisResult[]}
11
+ */
12
+ inputTracing.getResultsByRuleId = function(ruleId, context) {
13
+ return context.findings.resultsMap[ruleId];
14
+ };
15
+
16
+ /**
17
+ * Given a ruleId and an analysis function, wrap the analysis function
18
+ * with common setup and post-processing code.
19
+ */
20
+ inputTracing.handlerFactory = function(ruleId, analysisFn) {
21
+ /**
22
+ * This is the common API for INPUT TRACING instrumentation.
23
+ * @param {object} sourceContext
24
+ * @param {object} sinkContext
25
+ */
26
+ return function(sourceContext, sinkContext) {
27
+ if (sourceContext.rules.agentLibRules[ruleId].mode === 'off') {
28
+ return;
29
+ }
30
+
31
+ const results = inputTracing.getResultsByRuleId(ruleId, sourceContext);
32
+ if (!results) return;
33
+
34
+ for (const result of results) {
35
+ const findings = analysisFn(result, sinkContext);
36
+
37
+ if (findings) {
38
+ result.details.push({ sinkContext, findings });
39
+
40
+ if (sourceContext.rules.agentLibRules[ruleId].mode === 'block') {
41
+ result.blocked = true;
42
+ const blockInfo = ['block', ruleId];
43
+ sourceContext.findings.securityException = blockInfo;
44
+ throwSecurityException(sourceContext);
45
+ }
46
+ }
47
+ }
48
+ };
49
+ };
50
+
51
+ inputTracing.handlePathTraversal = inputTracing.handlerFactory(
52
+ 'path-traversal',
53
+ function(result, sinkContext) {
54
+ const idx = sinkContext.value.indexOf(result.value);
55
+ return idx !== -1 ? { path: sinkContext.value } : null;
56
+ }
57
+ );
58
+
59
+ inputTracing.handleCommandInjection = inputTracing.handlerFactory(
60
+ 'cmd-injection',
61
+ function(result, sinkContext) {
62
+ const inputIndex = sinkContext.value.indexOf(result.value);
63
+ if (inputIndex !== -1) {
64
+ return agentLib.checkCommandInjectionSink(
65
+ inputIndex,
66
+ result.value.length,
67
+ sinkContext.value,
68
+ );
69
+ }
70
+ }
71
+ );
72
+
73
+ inputTracing.handleSqlInjection = inputTracing.handlerFactory(
74
+ 'sql-injection',
75
+ function(result, sinkContext) {
76
+ let analysis = null;
77
+
78
+ const inputIndex = sinkContext.value.indexOf(result.value);
79
+ // if the user input is not in the sink input, there is nothing to do.
80
+ if (inputIndex === -1) {
81
+ return analysis;
82
+ }
83
+
84
+ if (inputIndex === 0 && sinkContext.value === result.value) {
85
+ analysis = {
86
+ startIndex: 0,
87
+ endIndex: result.value.length - 1,
88
+ overrunIndex: 0,
89
+ boundaryIndex: 0,
90
+ };
91
+ } else {
92
+ analysis = agentLib.checkSqlInjectionSink(
93
+ inputIndex,
94
+ result.value.length,
95
+ 2,
96
+ sinkContext.value,
97
+ );
98
+ }
99
+
100
+ return analysis;
101
+ }
102
+ );
103
+
104
+ inputTracing.nosqlInjectionMongo = inputTracing.handlerFactory(
105
+ 'nosql-injection-mongo', require('./nosql-injection-mongo')
106
+ );
107
+
108
+ inputTracing.ssjsInjection = inputTracing.handlerFactory(
109
+ 'ssjs-injection',
110
+ function(results, sinkContext) {
111
+ return null;
112
+ }
113
+ );
114
+
115
+ return inputTracing;
116
+ };
117
+
@@ -0,0 +1,395 @@
1
+ 'use strict';
2
+
3
+ const { expect } = require('chai');
4
+ const sinon = require('sinon');
5
+
6
+ describe('protect input-tracing handlers', function() {
7
+ let handlers;
8
+
9
+ // each test should be asserting this at least
10
+ const TEST_DESCRIPTION = 'captures findings and sinkContext in result.details array';
11
+
12
+ // for additional assertions that might happen in tests, add to description
13
+ function makeTestDescription(blocks) {
14
+ const addl = blocks ? ' and blocks when in BLOCK mode' : '';
15
+ return `${TEST_DESCRIPTION}${addl}`;
16
+ }
17
+
18
+ function makeSourceContext(resultsList, rules) {
19
+ const e = new Error('SecurityException');
20
+ const resultsMap = Object.create(null);
21
+ if (resultsList) {
22
+ for (const result of resultsList) {
23
+ if (!resultsMap[result.ruleId]) {
24
+ resultsMap[result.ruleId] = [];
25
+ }
26
+ resultsMap[result.ruleId].push(result);
27
+ }
28
+ }
29
+
30
+ return {
31
+ rules: {
32
+ agentLibRules: {
33
+ ['cmd-injection']: { mode: 'monitor' },
34
+ ['path-traversal']: { mode: 'monitor' },
35
+ ['sql-injection']: { mode: 'monitor' },
36
+ ['ssjs-injection']: { mode: 'monitor' },
37
+ ...(rules || {})
38
+ }
39
+ },
40
+ findings: {
41
+ trackRequest: true,
42
+ resultsMap,
43
+ },
44
+ block: sinon.stub().throws(e)
45
+ };
46
+ }
47
+
48
+ before(function() {
49
+ const mocks = require('../../../../test/mocks');
50
+
51
+ const core = mocks.core();
52
+ core.logger = mocks.logger();
53
+ core.protect = mocks.protect();
54
+
55
+ handlers = require('.')(core);
56
+ });
57
+
58
+ describe('getResultsByRuleId()', function() {
59
+ [
60
+ { findings: { resultsMap: {} } },
61
+ { findings: { resultsMap: { 'not foo': [{ ruleId: 'not foo' }] } } },
62
+ ].forEach((sourceContext) => {
63
+ it(`returns null when context has missing keys or empty results. context = ${JSON.stringify(sourceContext)}`, function() {
64
+ expect(handlers.getResultsByRuleId('foo', sourceContext)).eql(undefined);
65
+ });
66
+ });
67
+
68
+ it('returns all items from context\'s resultsList which match provided ruldId', function() {
69
+ const context = {
70
+ findings: {
71
+ resultsMap: {
72
+ 'foo': [
73
+ { ruleId: 'foo', tag: 1 },
74
+ { ruleId: 'foo' },
75
+ ],
76
+ 'not foo': [
77
+ { ruleId: 'not foo' }
78
+ ],
79
+ }
80
+ }
81
+ };
82
+ expect(handlers.getResultsByRuleId('foo', context)).eql([
83
+ { ruleId: 'foo', tag: 1 },
84
+ { ruleId: 'foo' }
85
+ ]);
86
+ });
87
+
88
+ it('noops when there are no matching ruleId values', function() {
89
+ const sourceContext = makeSourceContext([
90
+ {
91
+ ruleId: 'foo'
92
+ }
93
+ ]);
94
+ const sinkContext = {
95
+ name: 'bar',
96
+ value: 'baz',
97
+ };
98
+
99
+ handlers.handlePathTraversal(sourceContext, sinkContext);
100
+ handlers.handleCommandInjection(sourceContext, sinkContext);
101
+ handlers.handleSqlInjection(sourceContext, sinkContext);
102
+
103
+ expect(sourceContext.findings.resultsMap['foo'][0].details).undefined;
104
+ });
105
+ });
106
+
107
+ describe('handlePathTraversal()', function() {
108
+ const sinkContextPositive = {
109
+ name: 'fs.readFileSync',
110
+ value: './../../../etc/passwd',
111
+ stack: []
112
+ };
113
+ const sinkContextNegative = {
114
+ name: 'fs.readFileSync',
115
+ value: 'index.js',
116
+ stack: []
117
+ };
118
+ const findings = {
119
+ path: './../../../etc/passwd',
120
+ };
121
+
122
+ [
123
+ {
124
+ blocks: false,
125
+ sourceContext: makeSourceContext(
126
+ [{
127
+ ruleId: 'path-traversal',
128
+ value: '../../../etc/passwd',
129
+ details: [],
130
+ }]
131
+ ),
132
+ expectedDetails: [
133
+ {
134
+ sinkContext: sinkContextPositive,
135
+ findings,
136
+ },
137
+ {
138
+ sinkContext: sinkContextPositive,
139
+ findings,
140
+ }
141
+ ]
142
+ },
143
+ {
144
+ blocks: true,
145
+ sourceContext: makeSourceContext(
146
+ [{
147
+ ruleId: 'path-traversal',
148
+ value: '../../../etc/passwd',
149
+ details: [],
150
+ }],
151
+ {
152
+ ['path-traversal']: { mode: 'block' }
153
+ }
154
+ ),
155
+ expectedDetails: [
156
+ {
157
+ sinkContext: sinkContextPositive,
158
+ findings,
159
+ },
160
+ ]
161
+ },
162
+ {
163
+ blocks: false,
164
+ sourceContext: makeSourceContext(
165
+ [{
166
+ ruleId: 'path-traversal',
167
+ value: '../../../etc/passwd',
168
+ details: [],
169
+ }],
170
+ {
171
+ ['path-traversal']: { mode: 'off' }
172
+ }
173
+ ),
174
+ expectedDetails: []
175
+ }
176
+ ].forEach(({ blocks, sourceContext, expectedDetails }) => {
177
+ it(makeTestDescription(blocks), function() {
178
+ const testFn = () => {
179
+ handlers.handlePathTraversal(sourceContext, sinkContextPositive);
180
+ handlers.handlePathTraversal(sourceContext, sinkContextPositive);
181
+ handlers.handlePathTraversal(sourceContext, sinkContextNegative);
182
+ };
183
+
184
+ const test = expect(testFn);
185
+ blocks ? test.to.throw('SecurityException') : test.not.to.throw();
186
+
187
+ expect(sourceContext.findings.resultsMap['path-traversal'][0].details).to.eql(expectedDetails);
188
+ });
189
+ });
190
+ });
191
+
192
+ describe('handleCmdInjection()', function() {
193
+ const sinkContextPositive = {
194
+ name: 'child_process.execSync',
195
+ value: 'ls; cat /etc/passwd',
196
+ stack: [],
197
+ };
198
+ const sinkContextNegative = {
199
+ name: 'child_process.execSync',
200
+ value: 'foo',
201
+ stack: []
202
+ };
203
+ const findings = {
204
+ boundaryIndex: 2,
205
+ endIndex: 19,
206
+ overrunIndex: 4,
207
+ startIndex: 2,
208
+ };
209
+
210
+ [
211
+ {
212
+ blocks: false,
213
+ sourceContext: makeSourceContext(
214
+ [{
215
+ ruleId: 'cmd-injection',
216
+ value: '; cat /etc/passwd',
217
+ details: [],
218
+ }]
219
+ ),
220
+ expectedDetails: [
221
+ {
222
+ sinkContext: sinkContextPositive,
223
+ findings,
224
+ },
225
+ {
226
+ sinkContext: sinkContextPositive,
227
+ findings,
228
+ }
229
+ ]
230
+ },
231
+ {
232
+ blocks: true,
233
+ sourceContext: makeSourceContext(
234
+ [{
235
+ ruleId: 'cmd-injection',
236
+ value: '; cat /etc/passwd',
237
+ details: [],
238
+ }], {
239
+ ['cmd-injection']: { mode: 'block' }
240
+ }
241
+ ),
242
+ expectedDetails: [
243
+ {
244
+ sinkContext: sinkContextPositive,
245
+ findings,
246
+ },
247
+ ]
248
+ },
249
+ {
250
+ blocks: false,
251
+ sourceContext: makeSourceContext(
252
+ [{
253
+ ruleId: 'cmd-injection',
254
+ value: '; cat /etc/passwd',
255
+ details: [],
256
+ }], {
257
+ ['cmd-injection']: { mode: 'off' }
258
+ }
259
+ ),
260
+ expectedDetails: []
261
+ }
262
+ ].forEach(({ blocks, sourceContext, expectedDetails }) => {
263
+ it(makeTestDescription(blocks), function() {
264
+ const testFn = () => {
265
+ handlers.handleCommandInjection(sourceContext, sinkContextPositive);
266
+ handlers.handleCommandInjection(sourceContext, sinkContextPositive);
267
+ handlers.handleCommandInjection(sourceContext, sinkContextNegative);
268
+ };
269
+
270
+ const test = expect(testFn);
271
+ blocks ? test.to.throw('SecurityException') : test.not.to.throw();
272
+
273
+ const cmdiResults = sourceContext.findings.resultsMap['cmd-injection'];
274
+
275
+ expect(cmdiResults[0].details).to.eql(expectedDetails);
276
+ });
277
+ });
278
+ });
279
+
280
+ describe('handleSqlInjection()', function() {
281
+ const sinkContextPositive = {
282
+ name: 'mysql.query',
283
+ value: 'select * from foo where col = "" and 1 = 1; --',
284
+ stack: [],
285
+ };
286
+ const sinkContextNegative = {
287
+ name: 'mysql.query',
288
+ value: 'select 1',
289
+ stack: [],
290
+ };
291
+ const findings = {
292
+ boundaryIndex: 30,
293
+ endIndex: 46,
294
+ overrunIndex: 31,
295
+ startIndex: 31,
296
+ };
297
+
298
+ [
299
+ {
300
+ blocks: false,
301
+ findings,
302
+ sourceContext: makeSourceContext(
303
+ [
304
+ {
305
+ ruleId: 'sql-injection',
306
+ value: '" and 1 = 1; --',
307
+ details: [],
308
+ }
309
+ ]
310
+ ),
311
+ expectedDetails: [
312
+ {
313
+ sinkContext: sinkContextPositive,
314
+ findings,
315
+ },
316
+ {
317
+ sinkContext: sinkContextPositive,
318
+ findings,
319
+ }
320
+ ]
321
+ },
322
+ {
323
+ blocks: true,
324
+ findings: {
325
+ startIndex: 0,
326
+ endIndex: 30,
327
+ overrunIndex: 0,
328
+ boundaryIndex: 0,
329
+ },
330
+ sourceContext: makeSourceContext(
331
+ [
332
+ {
333
+ ruleId: 'sql-injection',
334
+ // this is the exact value of the query - this is to get coverage
335
+ value: 'select * from foo where col = "" and 1 = 1; --',
336
+ details: [],
337
+ }
338
+ ],
339
+ {
340
+ ['sql-injection']: { mode: 'block' }
341
+ }
342
+ ),
343
+ expectedDetails: [
344
+ {
345
+ sinkContext: sinkContextPositive,
346
+ findings: {
347
+ startIndex: 0,
348
+ endIndex: 45,
349
+ overrunIndex: 0,
350
+ boundaryIndex: 0,
351
+ },
352
+ },
353
+ ]
354
+ },
355
+ {
356
+ blocks: false,
357
+ findings: {
358
+ startIndex: 0,
359
+ endIndex: 30,
360
+ overrunIndex: 0,
361
+ boundaryIndex: 0,
362
+ },
363
+ sourceContext: makeSourceContext(
364
+ [
365
+ {
366
+ ruleId: 'sql-injection',
367
+ value: '" and 1 = 1; --',
368
+ details: [],
369
+ }
370
+ ],
371
+ {
372
+ ['sql-injection']: { mode: 'off' }
373
+ }
374
+ ),
375
+ expectedDetails: []
376
+ }
377
+ ].forEach(({ analyis, blocks, sourceContext, expectedDetails }) => {
378
+ it(makeTestDescription(blocks), function() {
379
+ const testFn = () => {
380
+ handlers.handleSqlInjection(sourceContext, sinkContextPositive);
381
+ handlers.handleSqlInjection(sourceContext, sinkContextPositive);
382
+ handlers.handleSqlInjection(sourceContext, sinkContextNegative);
383
+ };
384
+
385
+ const test = expect(testFn);
386
+ blocks ? test.to.throw('SecurityException') : test.not.to.throw();
387
+
388
+ const sqliResults = sourceContext.findings.resultsMap['sql-injection'];
389
+ expect(sqliResults[0].details).to.eql(expectedDetails);
390
+ });
391
+ });
392
+ });
393
+
394
+ it.skip('handleSsjsInjection()', function() {});
395
+ });
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ /* c8 ignore start */
4
+ // this is just the general structure of how to handle mongo sinks
5
+ const util = require('util');
6
+
7
+ const { simpleTraverse } = require('../../utils');
8
+
9
+ function mongoSink(results, sinkContext) {
10
+ if (typeof sinkContext.value === 'object') {
11
+ return handleObjectValue(results, sinkContext.value);
12
+ } else if (typeof sinkContext.value === 'string') {
13
+ return handleStringValue(results, sinkContext.value);
14
+ }
15
+
16
+ return null;
17
+ }
18
+
19
+ function handleObjectValue(results, object) {
20
+ for (const result of results) {
21
+ simpleTraverse(object, function(path, type, value) {
22
+ if (type !== 'Key') {
23
+ return;
24
+ }
25
+ // the result value is the key that was found
26
+ if (result.value === value) {
27
+ // does the object at this path equal the user input?
28
+ let obj = object;
29
+ for (const p of path) {
30
+ obj = obj[p];
31
+ }
32
+ obj = obj[value];
33
+ // does the found object in the query equal the saved object?
34
+ if (util.isDeepStrictEqual(obj, object)) {
35
+ //
36
+ }
37
+ }
38
+ });
39
+ }
40
+ }
41
+
42
+ function handleStringValue(results, string) {
43
+ // nyi
44
+ }
45
+
46
+ module.exports = mongoSink;
47
+
48
+ /* c8 ignore stop */
@@ -0,0 +1,32 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * INPUT TRACING is a STAGE of Protect.
5
+ * The specification can be found here https://protect-spec.prod.dotnet.contsec.com/guide/input-tracing.html.
6
+ *
7
+ * To view other STAGES see https://protect-spec.prod.dotnet.contsec.com/guide/protect-types.html#protection-types
8
+ * @param {object} core composed dependencies
9
+ * @returns {object}
10
+ */
11
+ module.exports = function(core) {
12
+ const inputTracing = core.protect.inputTracing = {};
13
+
14
+ // load the interfaces that will be used by input tracing instrumentation
15
+ require('./handlers')(core);
16
+
17
+ // load the instrumentation installers
18
+ require('./install/fs')(core);
19
+ require('./install/child-process')(core);
20
+ require('./install/mysql')(core);
21
+ require('./install/postgres')(core);
22
+
23
+ inputTracing.install = function() {
24
+ inputTracing.fsInstrumentation.install();
25
+ inputTracing.cpInstrumentation.install();
26
+ inputTracing.mysqlInstrumentation.install();
27
+ inputTracing.postgresInstrumentation.install();
28
+ // TODO: NODE-2360 (2260?)
29
+ };
30
+
31
+ return inputTracing;
32
+ };
@@ -0,0 +1 @@
1
+ This is where to place sinks for input-tracing rules.
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ const { isString } = require('@contrast/common');
4
+ const { patchType } = require('../constants');
5
+
6
+ module.exports = function(core) {
7
+ const {
8
+ scopes: { sources, instrumentation },
9
+ patcher,
10
+ depHooks,
11
+ captureStacktrace,
12
+ protect: { inputTracing }
13
+ } = core;
14
+
15
+ function install() {
16
+ depHooks.resolve({ name: 'child_process' }, cp => {
17
+ ['exec', 'execSync'].forEach((method) => {
18
+ const name = `child_process.${method}`;
19
+ patcher.patch(cp, method, {
20
+ name,
21
+ patchType,
22
+ pre(data) {
23
+ if (instrumentation.isLocked()) return;
24
+
25
+ const sourceContext = sources.getStore()?.protect;
26
+ if (!sourceContext) return;
27
+
28
+ const value = data.args[0];
29
+ if (!value || !isString(value)) return;
30
+
31
+ const sinkContext = captureStacktrace(
32
+ { name, value },
33
+ { constructorOpt: data.hooked }
34
+ );
35
+ inputTracing.handleCommandInjection(sourceContext, sinkContext);
36
+ }
37
+ });
38
+ });
39
+ });
40
+ }
41
+
42
+ const cpInstrumentation = inputTracing.cpInstrumentation = { install };
43
+
44
+ return cpInstrumentation;
45
+ };