@contrast/agent 4.2.0 → 4.4.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/bin/VERSION +1 -1
- package/bin/linux/contrast-service +0 -0
- package/bin/mac/contrast-service +0 -0
- package/bin/windows/contrast-service.exe +0 -0
- package/lib/assess/models/tag-range/index.js +6 -16
- package/lib/assess/policy/signatures.json +7 -0
- package/lib/assess/policy/util.js +9 -2
- package/lib/assess/propagators/manager.js +17 -3
- package/lib/assess/sinks/mongodb.js +11 -7
- package/lib/contrast.js +4 -5
- package/lib/core/config/options.js +7 -0
- package/lib/core/logger/perf-logger.js +3 -1
- package/lib/hooks/express-fileupload.js +57 -0
- package/lib/hooks/patcher.js +2 -1
- package/lib/instrumentation.js +1 -0
- package/lib/libraries.js +7 -3
- package/lib/protect/analysis/aho-corasick.js +192 -0
- package/lib/protect/analysis/dfsa-analyzer.js +64 -0
- package/lib/protect/input-analysis.js +1 -0
- package/lib/protect/service.js +13 -0
- package/lib/reporter/models/app-update/index.js +2 -4
- package/node_modules/unix-dgram/build/Makefile +1 -1
- package/node_modules/unix-dgram/build/config.gypi +1 -1
- package/package.json +6 -6
package/bin/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.26.0
|
|
Binary file
|
package/bin/mac/contrast-service
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -14,7 +14,6 @@ Copyright: 2021 Contrast Security, Inc
|
|
|
14
14
|
*/
|
|
15
15
|
'use strict';
|
|
16
16
|
|
|
17
|
-
const _ = require('lodash');
|
|
18
17
|
const logger = require('../../../core/logger')('contrast:tagRange');
|
|
19
18
|
|
|
20
19
|
const Relationships = require('./relationships');
|
|
@@ -27,14 +26,13 @@ const DEFAULT_TAG = 'untrusted';
|
|
|
27
26
|
*/
|
|
28
27
|
class TagRange {
|
|
29
28
|
/**
|
|
30
|
-
*
|
|
31
|
-
* @param {number}
|
|
32
|
-
* @param {
|
|
33
|
-
* @param {string} tag The name of the tag.
|
|
29
|
+
* @param {number} start The starting index of string tracking on the data having the tag.
|
|
30
|
+
* @param {number} stop The stopping index of string tracking on the data having the tag.
|
|
31
|
+
* @param {string?} tag The name of the tag (default is "untrusted").
|
|
34
32
|
*/
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (start
|
|
33
|
+
constructor(start, stop, tag = DEFAULT_TAG) {
|
|
34
|
+
// Validates the arguments to the contructor call.
|
|
35
|
+
if (!(start <= stop && start >= 0)) {
|
|
38
36
|
logger.debug(
|
|
39
37
|
'could not create tag %s with invalid range start: %s, stop %s.',
|
|
40
38
|
tag,
|
|
@@ -42,15 +40,7 @@ class TagRange {
|
|
|
42
40
|
stop
|
|
43
41
|
);
|
|
44
42
|
}
|
|
45
|
-
}
|
|
46
43
|
|
|
47
|
-
/**
|
|
48
|
-
* @param {number} start The starting index of string tracking on the data having the tag.
|
|
49
|
-
* @param {number} stop The stopping index of string tracking on the data having the tag.
|
|
50
|
-
* @param {string?} tag The name of the tag (default is "untrusted").
|
|
51
|
-
*/
|
|
52
|
-
constructor(start, stop, tag = DEFAULT_TAG) {
|
|
53
|
-
TagRange.validate(start, stop, tag);
|
|
54
44
|
/** @type {string} */
|
|
55
45
|
this.tag = tag;
|
|
56
46
|
/** @type {number} */
|
|
@@ -125,6 +125,11 @@
|
|
|
125
125
|
"methodName": "",
|
|
126
126
|
"isModule": true
|
|
127
127
|
},
|
|
128
|
+
"express-fileupload": {
|
|
129
|
+
"moduleName": "express-fileupload",
|
|
130
|
+
"methodName": "",
|
|
131
|
+
"isModule": true
|
|
132
|
+
},
|
|
128
133
|
"pg.Connection.prototype.query": {
|
|
129
134
|
"moduleName": "pg",
|
|
130
135
|
"methodName": "Connection.prototype.query",
|
|
@@ -392,11 +397,13 @@
|
|
|
392
397
|
},
|
|
393
398
|
"ejs.Template.prototype.generateSource": {
|
|
394
399
|
"moduleName": "ejs",
|
|
400
|
+
"version": ">=2.6.2",
|
|
395
401
|
"methodName": "Template.prototype.generateSource",
|
|
396
402
|
"isModule": true
|
|
397
403
|
},
|
|
398
404
|
"ejs.utils.escapeXML": {
|
|
399
405
|
"moduleName": "ejs",
|
|
406
|
+
"version": ">=2.6.2",
|
|
400
407
|
"fileName": "lib/utils.js",
|
|
401
408
|
"methodName": "escapeXML",
|
|
402
409
|
"isModule": true
|
|
@@ -372,12 +372,19 @@ utils.createHookFromSignature = function(signature, options, patchType) {
|
|
|
372
372
|
requireHook.resolve(
|
|
373
373
|
{
|
|
374
374
|
name: signature.moduleName,
|
|
375
|
-
file: signature.fileName
|
|
375
|
+
file: signature.fileName,
|
|
376
|
+
version: signature.version
|
|
376
377
|
},
|
|
377
378
|
requireCallback
|
|
378
379
|
);
|
|
379
380
|
} else {
|
|
380
|
-
requireHook.resolve(
|
|
381
|
+
requireHook.resolve(
|
|
382
|
+
{
|
|
383
|
+
name: signature.moduleName,
|
|
384
|
+
version: signature.version
|
|
385
|
+
},
|
|
386
|
+
requireCallback
|
|
387
|
+
);
|
|
381
388
|
}
|
|
382
389
|
} else {
|
|
383
390
|
const mod = global[signature.moduleName],
|
|
@@ -396,10 +396,24 @@ function isTargetTracked(target, hasTags) {
|
|
|
396
396
|
* @return {Array} all valid targets based on type
|
|
397
397
|
*/
|
|
398
398
|
function extractValidTarget(target, data) {
|
|
399
|
-
|
|
400
|
-
|
|
399
|
+
let validTarget = null;
|
|
400
|
+
switch (target) {
|
|
401
|
+
case 'R':
|
|
402
|
+
validTarget = data.result;
|
|
403
|
+
break;
|
|
404
|
+
default:
|
|
405
|
+
logger.warn(
|
|
406
|
+
'Invalid target type %s for propagator %s',
|
|
407
|
+
target,
|
|
408
|
+
data.name
|
|
409
|
+
);
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (isString(validTarget) && validTarget.length > 0) {
|
|
414
|
+
return validTarget;
|
|
401
415
|
}
|
|
402
|
-
|
|
416
|
+
|
|
403
417
|
return null;
|
|
404
418
|
}
|
|
405
419
|
/**
|
|
@@ -33,6 +33,7 @@ const { PATCH_TYPES } = require('../../constants');
|
|
|
33
33
|
const requireHook = require('../../hooks/require');
|
|
34
34
|
const { Signature, CallContext } = require('../models');
|
|
35
35
|
const policy = require('../policy');
|
|
36
|
+
const Scopes = require('../../core/async-storage/scopes');
|
|
36
37
|
|
|
37
38
|
const ruleId = 'nosql-injection';
|
|
38
39
|
const disallowedTags = [
|
|
@@ -268,13 +269,16 @@ module.exports = ({ common }) => {
|
|
|
268
269
|
*/
|
|
269
270
|
mongoSink.assess = (query, context) => {
|
|
270
271
|
const searchDepth = 3;
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
272
|
+
let vulnerableString;
|
|
273
|
+
|
|
274
|
+
Scopes.runInAllowAllScope(() => {
|
|
275
|
+
vulnerableString = common.isVulnerable({
|
|
276
|
+
searchDepth,
|
|
277
|
+
disallowedTags,
|
|
278
|
+
requiredTags,
|
|
279
|
+
input: query
|
|
280
|
+
});
|
|
281
|
+
}, 'mongodbSink.assess');
|
|
278
282
|
|
|
279
283
|
if (vulnerableString) {
|
|
280
284
|
mongoSink.report(vulnerableString, context);
|
package/lib/contrast.js
CHANGED
|
@@ -403,15 +403,14 @@ contrastAgent.run = function(nodePath, script) {
|
|
|
403
403
|
* @param {String} script The path to the application's entry point.
|
|
404
404
|
*/
|
|
405
405
|
contrastAgent.resetArgs = function(nodePath, script) {
|
|
406
|
-
const appArgs = agent.config.application.args;
|
|
407
|
-
const isPrimary = !agent.hasOwnProperty('cluster') || agent.cluster.isPrimary;
|
|
408
406
|
script = path.resolve(script);
|
|
407
|
+
process.argv = agent.config
|
|
408
|
+
? [nodePath, script].concat(agent.config.application.args)
|
|
409
|
+
: process.argv;
|
|
410
|
+
const isPrimary = !agent.hasOwnProperty('cluster') || agent.cluster.isPrimary;
|
|
409
411
|
const location = isPrimary ? 'Entering main' : 'Entering fork';
|
|
410
412
|
|
|
411
413
|
logger.debug('%s at %s', location, script);
|
|
412
|
-
|
|
413
|
-
// need to set process.argv to what it would be without our agent args
|
|
414
|
-
process.argv = [nodePath, script].concat(appArgs);
|
|
415
414
|
};
|
|
416
415
|
|
|
417
416
|
/**
|
|
@@ -419,6 +419,13 @@ const agent = [
|
|
|
419
419
|
fn: castBoolean,
|
|
420
420
|
desc: 'whether to use speedracer for input analysis when enabled'
|
|
421
421
|
},
|
|
422
|
+
{
|
|
423
|
+
name: 'agent.node.native_input_analysis',
|
|
424
|
+
arg: '[true]',
|
|
425
|
+
default: false,
|
|
426
|
+
fn: castBoolean,
|
|
427
|
+
desc: 'do agent-native input analysis prior to any external analysis'
|
|
428
|
+
},
|
|
422
429
|
{
|
|
423
430
|
name: 'agent.node.unsafe.deadzones',
|
|
424
431
|
arg: '<modules>',
|
|
@@ -157,7 +157,9 @@ const STORAGE_KEY = 'REQ_PERF_DATA';
|
|
|
157
157
|
|
|
158
158
|
module.exports = (agent) => {
|
|
159
159
|
const enabled =
|
|
160
|
-
agent.config
|
|
160
|
+
agent.config &&
|
|
161
|
+
agent.config.agent.node.req_perf_logging &&
|
|
162
|
+
process.hrtime.bigint;
|
|
161
163
|
if (enabled) {
|
|
162
164
|
agentEmitter.on('http.requestStart', (req) => {
|
|
163
165
|
AsyncStorage.set(STORAGE_KEY, new RequestStats(req));
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Copyright: 2021 Contrast Security, Inc
|
|
3
|
+
Contact: support@contrastsecurity.com
|
|
4
|
+
License: Commercial
|
|
5
|
+
|
|
6
|
+
NOTICE: This Software and the patented inventions embodied within may only be
|
|
7
|
+
used as part of Contrast Security’s commercial offerings. Even though it is
|
|
8
|
+
made available through public repositories, use of this Software is subject to
|
|
9
|
+
the applicable End User Licensing Agreement found at
|
|
10
|
+
https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
|
|
11
|
+
between Contrast Security and the End User. The Software may not be reverse
|
|
12
|
+
engineered, modified, repackaged, sold, redistributed or otherwise used in a
|
|
13
|
+
way not consistent with the End User License Agreement.
|
|
14
|
+
*/
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const patcher = require('../hooks/patcher');
|
|
18
|
+
const { PATCH_TYPES } = require('../constants');
|
|
19
|
+
const requireHook = require('../hooks/require');
|
|
20
|
+
const agentEmitter = require('../agent-emitter');
|
|
21
|
+
const logger = require('../core/logger')('contrast:hooks:express');
|
|
22
|
+
|
|
23
|
+
function hook() {
|
|
24
|
+
requireHook.resolve({ name: 'express-fileupload' }, (fileupload) => {
|
|
25
|
+
logger.info('hooking express-fileupload middleware');
|
|
26
|
+
const hooked = patcher.patch(fileupload, {
|
|
27
|
+
name: 'express-fileupload',
|
|
28
|
+
patchType: PATCH_TYPES.FRAMEWORK,
|
|
29
|
+
alwaysRun: true,
|
|
30
|
+
post(data) {
|
|
31
|
+
data.result = patcher.patch(data.result, {
|
|
32
|
+
name: 'express.hookedFileUploadMiddleware',
|
|
33
|
+
patchType: PATCH_TYPES.FRAMEWORK,
|
|
34
|
+
alwaysRun: true,
|
|
35
|
+
pre(data) {
|
|
36
|
+
const [req, , next] = data.args;
|
|
37
|
+
data.args[2] = function contrastNext() {
|
|
38
|
+
try {
|
|
39
|
+
if (req.files) {
|
|
40
|
+
agentEmitter.emit('assess.body', req, 'files');
|
|
41
|
+
}
|
|
42
|
+
} catch (err) {
|
|
43
|
+
logger.info(`Unable to patch express-fileupload. ${err}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
next();
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return hooked;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = hook;
|
package/lib/hooks/patcher.js
CHANGED
|
@@ -27,7 +27,8 @@ const logger = require('../core/logger')('contrast:hooks');
|
|
|
27
27
|
const agent = require('../agent');
|
|
28
28
|
const perfLogger = require('../core/logger/perf-logger')(agent);
|
|
29
29
|
const _ = require('lodash');
|
|
30
|
-
const perfLoggingEnabled =
|
|
30
|
+
const perfLoggingEnabled =
|
|
31
|
+
agent.config && agent.config.agent.node.req_perf_logging;
|
|
31
32
|
const tracker = require('../tracker.js');
|
|
32
33
|
const { AsyncStorage } = require('../core/async-storage');
|
|
33
34
|
const {
|
package/lib/instrumentation.js
CHANGED
package/lib/libraries.js
CHANGED
|
@@ -66,8 +66,12 @@ function setRequiredBy(deps, requiredBy = new Map()) {
|
|
|
66
66
|
* @param {Object} agent agent instance
|
|
67
67
|
*/
|
|
68
68
|
function processDependencies(deps, requiredBy, agent) {
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
for (const key in deps) {
|
|
70
|
+
const dep = deps[key];
|
|
71
|
+
if (!dep.name) {
|
|
72
|
+
dep.name = key;
|
|
73
|
+
}
|
|
74
|
+
|
|
71
75
|
dep._requiredBy = requiredBy.has(dep.name)
|
|
72
76
|
? Array.from(requiredBy.get(dep.name))
|
|
73
77
|
: [];
|
|
@@ -77,7 +81,7 @@ function processDependencies(deps, requiredBy, agent) {
|
|
|
77
81
|
if (newLib) {
|
|
78
82
|
processDependencies(dep.dependencies, requiredBy, agent);
|
|
79
83
|
}
|
|
80
|
-
}
|
|
84
|
+
}
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
/**
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Copyright: 2021 Contrast Security, Inc
|
|
3
|
+
Contact: support@contrastsecurity.com
|
|
4
|
+
License: Commercial
|
|
5
|
+
|
|
6
|
+
NOTICE: This Software and the patented inventions embodied within may only be
|
|
7
|
+
used as part of Contrast Security’s commercial offerings. Even though it is
|
|
8
|
+
made available through public repositories, use of this Software is subject to
|
|
9
|
+
the applicable End User Licensing Agreement found at
|
|
10
|
+
https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
|
|
11
|
+
between Contrast Security and the End User. The Software may not be reverse
|
|
12
|
+
engineered, modified, repackaged, sold, redistributed or otherwise used in a
|
|
13
|
+
way not consistent with the End User License Agreement.
|
|
14
|
+
*/
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
// https://www.geeksforgeeks.org/aho-corasick-algorithm-pattern-searching/
|
|
18
|
+
|
|
19
|
+
const a = 'a'.charCodeAt(0);
|
|
20
|
+
const z = 'z'.charCodeAt(0);
|
|
21
|
+
const A = 'A'.charCodeAt(0);
|
|
22
|
+
const Z = 'Z'.charCodeAt(0);
|
|
23
|
+
|
|
24
|
+
class AhoCorasick {
|
|
25
|
+
constructor(words) {
|
|
26
|
+
// the maximum number of states is equal to the sum of the length of
|
|
27
|
+
// the strings to be matched.
|
|
28
|
+
this.maxStates = 0;
|
|
29
|
+
for (const word of words) {
|
|
30
|
+
this.maxStates += word.length;
|
|
31
|
+
for (const char of word) {
|
|
32
|
+
// allow for any character to be upper or lower case. this could be
|
|
33
|
+
// restricted to certain words if desired, by changing the signature
|
|
34
|
+
// to {caseInsensitive, caseSensitive}.
|
|
35
|
+
if (char >= 'a' && char <= 'z') {
|
|
36
|
+
this.maxStates += 1;
|
|
37
|
+
} else if (char >= 'A' && char <= 'Z') {
|
|
38
|
+
this.maxStates += 1;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// can optimize this by trading off computation for space.
|
|
44
|
+
this.maxChars = 128;
|
|
45
|
+
|
|
46
|
+
// if this state matches words each match will be in an array at
|
|
47
|
+
// the state.
|
|
48
|
+
this.out = Array(this.maxStates + 1);
|
|
49
|
+
for (let i = 0; i <= this.maxStates; i++) {
|
|
50
|
+
this.out[i] = [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// state to transition to on failure
|
|
54
|
+
this.fail = Array(this.maxStates + 1);
|
|
55
|
+
|
|
56
|
+
// maxStates + 1 rows
|
|
57
|
+
// maxChars columns
|
|
58
|
+
this.goto = Array(this.maxStates + 1);
|
|
59
|
+
for (let i = 0; i <= this.maxStates; i++) {
|
|
60
|
+
this.goto[i] = Array();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this.words = words;
|
|
64
|
+
|
|
65
|
+
this.stateCount = this.buildAutomata();
|
|
66
|
+
|
|
67
|
+
// keep state so this can be called in a streaming fashion
|
|
68
|
+
this.state = 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
reset() {
|
|
72
|
+
this.state = 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* eslint-disable complexity */
|
|
76
|
+
buildAutomata() {
|
|
77
|
+
// only the root state (state 0) to start with
|
|
78
|
+
let stateCount = 1;
|
|
79
|
+
|
|
80
|
+
// fill in the goto matrix.
|
|
81
|
+
for (let i = 0; i < this.words.length; i++) {
|
|
82
|
+
const word = Buffer.from(this.words[i]);
|
|
83
|
+
let state = 0;
|
|
84
|
+
|
|
85
|
+
// create transitions for all character of the current word
|
|
86
|
+
for (const byte of word) {
|
|
87
|
+
if (byte & 0x80) {
|
|
88
|
+
throw new Error('pattern character codes cannot exceed 127');
|
|
89
|
+
}
|
|
90
|
+
if (this.goto[state][byte] === undefined) {
|
|
91
|
+
this.goto[state][byte] = stateCount;
|
|
92
|
+
stateCount += 1;
|
|
93
|
+
}
|
|
94
|
+
const previousState = state;
|
|
95
|
+
state = this.goto[state][byte];
|
|
96
|
+
|
|
97
|
+
// now make it case insensitive by mapping the alternate case to the
|
|
98
|
+
// same state as the original case.
|
|
99
|
+
let extra;
|
|
100
|
+
if (byte >= a && byte <= z) {
|
|
101
|
+
extra = byte - (a - A);
|
|
102
|
+
} else if (byte >= A && byte <= Z) {
|
|
103
|
+
extra = byte + (a - A);
|
|
104
|
+
} else {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (this.goto[previousState][extra] === undefined) {
|
|
109
|
+
// transition to the state that the other case character transitioned
|
|
110
|
+
// to.
|
|
111
|
+
this.goto[previousState][extra] = stateCount - 1;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// add current word to terminal list
|
|
116
|
+
this.out[state].push(this.words[i]);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// for all byte values that don't have a transition from the root, add
|
|
120
|
+
// a transition to the root.
|
|
121
|
+
for (let byte = 0; byte < this.maxChars; byte++) {
|
|
122
|
+
if (this.goto[0][byte] === undefined) {
|
|
123
|
+
this.goto[0][byte] = 0;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// failure function is computer using breadth first order.
|
|
128
|
+
const queue = [];
|
|
129
|
+
|
|
130
|
+
// iterate over all possible inputs
|
|
131
|
+
for (let i = 0; i < this.maxChars; i++) {
|
|
132
|
+
// all nodes of depth 1 have failure function value 0.
|
|
133
|
+
if (this.goto[0][i] !== 0) {
|
|
134
|
+
this.fail[this.goto[0][i]] = 0;
|
|
135
|
+
queue.push(this.goto[0][i]);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
while (queue.length) {
|
|
140
|
+
// get the first state in the queue
|
|
141
|
+
const state = queue.shift();
|
|
142
|
+
|
|
143
|
+
// for the removed state, find the failure function for all
|
|
144
|
+
// characters for which a goto is not defined.
|
|
145
|
+
for (let i = 0; i < this.maxChars; i++) {
|
|
146
|
+
if (this.goto[state][i] === undefined) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// get failure transition
|
|
151
|
+
let failure = this.fail[state];
|
|
152
|
+
|
|
153
|
+
// find the deepest node ..
|
|
154
|
+
|
|
155
|
+
while (this.goto[failure][i] === undefined) {
|
|
156
|
+
failure = this.fail[failure];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
failure = this.goto[failure][i];
|
|
160
|
+
this.fail[this.goto[state][i]] = failure;
|
|
161
|
+
|
|
162
|
+
// merge outputs
|
|
163
|
+
this.out[this.goto[state][i]].push(...this.out[failure]);
|
|
164
|
+
|
|
165
|
+
// insert the next level node (of trie) into queue
|
|
166
|
+
queue.push(this.goto[state][i]);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return stateCount;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
check(byte) {
|
|
174
|
+
// if it's > maxChars set it to a non-matching value to force failure.
|
|
175
|
+
// this could allow some memory-reduction optimizations if worth it. i.e.,
|
|
176
|
+
// maxChars only needs to be the highest charcode in the words the user
|
|
177
|
+
// passes in. and they can be offset by the lowest charcode as well.
|
|
178
|
+
if (byte >= this.maxChars) {
|
|
179
|
+
byte = 0;
|
|
180
|
+
}
|
|
181
|
+
let next = this.state;
|
|
182
|
+
while (this.goto[next][byte] === undefined) {
|
|
183
|
+
next = this.fail[next];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
this.state = this.goto[next][byte];
|
|
187
|
+
|
|
188
|
+
return this.out[this.state].length ? this.out[this.state] : null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
module.exports = AhoCorasick;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Copyright: 2021 Contrast Security, Inc
|
|
3
|
+
Contact: support@contrastsecurity.com
|
|
4
|
+
License: Commercial
|
|
5
|
+
|
|
6
|
+
NOTICE: This Software and the patented inventions embodied within may only be
|
|
7
|
+
used as part of Contrast Security’s commercial offerings. Even though it is
|
|
8
|
+
made available through public repositories, use of this Software is subject to
|
|
9
|
+
the applicable End User Licensing Agreement found at
|
|
10
|
+
https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
|
|
11
|
+
between Contrast Security and the End User. The Software may not be reverse
|
|
12
|
+
engineered, modified, repackaged, sold, redistributed or otherwise used in a
|
|
13
|
+
way not consistent with the End User License Agreement.
|
|
14
|
+
*/
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const AhoCorasick = require('./aho-corasick');
|
|
18
|
+
|
|
19
|
+
const defaultSuspicious = ";$'</\\&#%>=*(|`".split('');
|
|
20
|
+
defaultSuspicious.push('--', 'shell.', 'union select');
|
|
21
|
+
|
|
22
|
+
class Analyzer {
|
|
23
|
+
constructor(suspicious = defaultSuspicious, options = {}) {
|
|
24
|
+
this.options = options;
|
|
25
|
+
this.dfsa = Analyzer.prebuilt.get(suspicious);
|
|
26
|
+
if (!this.dfsa) {
|
|
27
|
+
this.dfsa = new AhoCorasick(suspicious);
|
|
28
|
+
Analyzer.prebuilt.set(suspicious, this.dfsa);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
suspicious(buffer) {
|
|
33
|
+
const found = new Set();
|
|
34
|
+
for (let ix = 0; ix < buffer.length; ix++) {
|
|
35
|
+
const result = this.dfsa.check(buffer[ix]);
|
|
36
|
+
if (result) {
|
|
37
|
+
for (const pattern of result) {
|
|
38
|
+
found.add(pattern);
|
|
39
|
+
}
|
|
40
|
+
if (this.options.returnOnFirstMatch) {
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (found.size) {
|
|
46
|
+
return [...found.values()];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
reset() {
|
|
53
|
+
this.dfsa.reset();
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
static reset() {
|
|
57
|
+
Analyzer.prebuilt.clear();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// can't initialize class statics.
|
|
62
|
+
Analyzer.prebuilt = new Map();
|
|
63
|
+
|
|
64
|
+
module.exports = Analyzer;
|
package/lib/protect/service.js
CHANGED
|
@@ -31,6 +31,7 @@ const headerValidators = require('./validators');
|
|
|
31
31
|
const UserInputKit = require('../reporter/models/utils/user-input-kit');
|
|
32
32
|
const UserInputFactory = require('../reporter/models/utils/user-input-factory');
|
|
33
33
|
const blockRequest = require('../util/block-request');
|
|
34
|
+
const Analyzer = require('./analysis/dfsa-analyzer');
|
|
34
35
|
|
|
35
36
|
class ProtectService {
|
|
36
37
|
/**
|
|
@@ -42,6 +43,7 @@ class ProtectService {
|
|
|
42
43
|
this.config = agent.config;
|
|
43
44
|
this.enabled = agent.isInDefendMode();
|
|
44
45
|
this.assessEnabled = agent.isInAssessMode();
|
|
46
|
+
this.nativeAnalysis = this.config.agent.node.native_input_analysis;
|
|
45
47
|
|
|
46
48
|
this._exclusionFactory = new ExclusionFactory({
|
|
47
49
|
featureSet: agent.tsFeatureSet,
|
|
@@ -106,6 +108,17 @@ class ProtectService {
|
|
|
106
108
|
analyzeRequestStream({ meta, req, res, appContext }) {
|
|
107
109
|
const { requestId, chunks } = meta;
|
|
108
110
|
|
|
111
|
+
if (this.nativeAnalysis) {
|
|
112
|
+
const analyzer = new Analyzer();
|
|
113
|
+
// if nothing is suspicious then don't send the body for analysis
|
|
114
|
+
if (!chunks.some((b) => analyzer.suspicious(b))) {
|
|
115
|
+
return Promise.resolve(true);
|
|
116
|
+
} else {
|
|
117
|
+
// do some processing on the data. tbd in wasm/rust/napi. potentially
|
|
118
|
+
// replace entire "send to service".
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
109
122
|
return this.reporter
|
|
110
123
|
.sendMessage('request', { requestId, chunks })
|
|
111
124
|
.then((agentSettings) =>
|
|
@@ -90,11 +90,9 @@ module.exports = class AppUpdate {
|
|
|
90
90
|
|
|
91
91
|
// exists somewhere else and DOES have a shasum. so, we check if we can find a hash/key, and then
|
|
92
92
|
// we check if the library has been seen or not.
|
|
93
|
-
if (!
|
|
93
|
+
if (!version) {
|
|
94
94
|
logger.info(
|
|
95
|
-
`package
|
|
96
|
-
name ? name : 'unknown'
|
|
97
|
-
} version: ${version ? version : 'unknown'}`,
|
|
95
|
+
`package: ${name} is missing version in package.json or it might not have been installed; unable to report lib.`,
|
|
98
96
|
data
|
|
99
97
|
);
|
|
100
98
|
return false;
|
|
@@ -309,7 +309,7 @@ endif
|
|
|
309
309
|
|
|
310
310
|
quiet_cmd_regen_makefile = ACTION Regenerating $@
|
|
311
311
|
cmd_regen_makefile = cd $(srcdir); /opt/hostedtoolcache/node/12.22.6/x64/lib/node_modules/npm/node_modules/node-gyp/gyp/gyp_main.py -fmake --ignore-environment "-Dlibrary=shared_library" "-Dvisibility=default" "-Dnode_root_dir=/home/runner/.cache/node-gyp/12.22.6" "-Dnode_gyp_dir=/opt/hostedtoolcache/node/12.22.6/x64/lib/node_modules/npm/node_modules/node-gyp" "-Dnode_lib_file=/home/runner/.cache/node-gyp/12.22.6/<(target_arch)/node.lib" "-Dmodule_root_dir=/home/runner/work/node-agent/node-agent/target/node_modules/unix-dgram" "-Dnode_engine=v8" "--depth=." "-Goutput_dir=." "--generator-output=build" -I/home/runner/work/node-agent/node-agent/target/node_modules/unix-dgram/build/config.gypi -I/opt/hostedtoolcache/node/12.22.6/x64/lib/node_modules/npm/node_modules/node-gyp/addon.gypi -I/home/runner/.cache/node-gyp/12.22.6/include/node/common.gypi "--toplevel-dir=." binding.gyp
|
|
312
|
-
Makefile: $(srcdir)/
|
|
312
|
+
Makefile: $(srcdir)/../../../../../../../../opt/hostedtoolcache/node/12.22.6/x64/lib/node_modules/npm/node_modules/node-gyp/addon.gypi $(srcdir)/../../../../../../.cache/node-gyp/12.22.6/include/node/common.gypi $(srcdir)/binding.gyp $(srcdir)/build/config.gypi
|
|
313
313
|
$(call do_cmd,regen_makefile)
|
|
314
314
|
|
|
315
315
|
# "all" is a concatenation of the "all" targets from all the included
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/agent",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.1",
|
|
4
4
|
"description": "Node.js security instrumentation by Contrast Security",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"security",
|
|
@@ -71,13 +71,13 @@
|
|
|
71
71
|
"@babel/types": "^7.12.1",
|
|
72
72
|
"@contrast/distringuish-prebuilt": "^2.2.0",
|
|
73
73
|
"@contrast/flat": "^4.1.1",
|
|
74
|
-
"@contrast/fn-inspect": "^2.4.
|
|
74
|
+
"@contrast/fn-inspect": "^2.4.2",
|
|
75
75
|
"@contrast/heapdump": "^1.1.0",
|
|
76
76
|
"@contrast/protobuf-api": "^3.2.0",
|
|
77
|
-
"@contrast/require-hook": "^2.0.
|
|
77
|
+
"@contrast/require-hook": "^2.0.5",
|
|
78
78
|
"@contrast/synchronous-source-maps": "^1.1.0",
|
|
79
79
|
"amqp-connection-manager": "^3.2.2",
|
|
80
|
-
"amqplib": "^0.
|
|
80
|
+
"amqplib": "^0.8.0",
|
|
81
81
|
"base64url": "^3.0.1",
|
|
82
82
|
"big-integer": "^1.6.36",
|
|
83
83
|
"bluebird": "^3.5.3",
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
"@bmacnaughton/string-generator": "^1.0.0",
|
|
113
113
|
"@contrast/eslint-config": "^2.0.1",
|
|
114
114
|
"@contrast/fake-module": "file:test/mock/contrast-fake",
|
|
115
|
-
"@contrast/screener-service": "^1.12.
|
|
115
|
+
"@contrast/screener-service": "^1.12.4",
|
|
116
116
|
"@hapi/boom": "file:test/mock/boom",
|
|
117
117
|
"@hapi/hapi": "file:test/mock/hapi",
|
|
118
118
|
"@ls-lint/ls-lint": "^1.8.1",
|
|
@@ -183,7 +183,7 @@
|
|
|
183
183
|
"test": "test"
|
|
184
184
|
},
|
|
185
185
|
"engines": {
|
|
186
|
-
"node": ">=12.13.0 <13 || >=14.15.0 <15 || >=16.
|
|
186
|
+
"node": ">=12.13.0 <13 || >=14.15.0 <15 || >=16.9.1 <17",
|
|
187
187
|
"npm": ">=6.13.7 <7 || >=7.11.0"
|
|
188
188
|
},
|
|
189
189
|
"bundleDependencies": [
|