@contrast/agent 4.19.3 → 4.19.6
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/lib/assess/hapi/sinks/xss.js +2 -2
- package/lib/assess/loopback4/route-coverage.js +2 -2
- package/lib/assess/models/call-context.js +7 -4
- package/lib/core/async-storage/index.js +1 -0
- package/lib/core/express/utils.js +1 -1
- package/lib/library-usage.js +1 -1
- package/lib/list-installed.js +47 -23
- package/lib/protect/express/index.js +6 -2
- package/lib/protect/express/utils.js +60 -0
- package/lib/protect/service.js +28 -5
- package/lib/reporter/translations/to-protobuf/dtm/raw-request.js +8 -2
- package/package.json +6 -6
|
@@ -25,7 +25,7 @@ const {
|
|
|
25
25
|
const stackFactory = require('../../../core/stacktrace').singleton;
|
|
26
26
|
const { AsyncStorage, KEYS } = require('../../../core/async-storage');
|
|
27
27
|
const semver = require('semver');
|
|
28
|
-
const {
|
|
28
|
+
const { funcInfo } = require('@contrast/fn-inspect');
|
|
29
29
|
const { PATCH_TYPES } = require('../../../constants');
|
|
30
30
|
|
|
31
31
|
class HapiXssSink {
|
|
@@ -107,7 +107,7 @@ class HapiXssSink {
|
|
|
107
107
|
// if route coverage is enabled we put the original function
|
|
108
108
|
// on the wrap in a Symbol, use that if it exists
|
|
109
109
|
handler = handler[ORIG_FUNC] || handler;
|
|
110
|
-
const topFrame =
|
|
110
|
+
const topFrame = funcInfo(handler);
|
|
111
111
|
const stacktrace = stackFactory.createSnapshot({
|
|
112
112
|
constructorOpt: data.hooked,
|
|
113
113
|
prependFrames: [topFrame]
|
|
@@ -19,7 +19,7 @@ const agentEmitter = require('../../agent-emitter');
|
|
|
19
19
|
const patcher = require('../../hooks/patcher');
|
|
20
20
|
const moduleHook = require('../../hooks/require');
|
|
21
21
|
const { PATCH_TYPES } = require('../../constants');
|
|
22
|
-
const {
|
|
22
|
+
const { funcInfo } = require('@contrast/fn-inspect');
|
|
23
23
|
|
|
24
24
|
class RouteCoverage {
|
|
25
25
|
constructor(agent) {
|
|
@@ -54,7 +54,7 @@ class RouteCoverage {
|
|
|
54
54
|
*/
|
|
55
55
|
getSignatureFunc(route) {
|
|
56
56
|
const func = route._controllerName ? route._controllerCtor : route._handler;
|
|
57
|
-
const finfo =
|
|
57
|
+
const finfo = funcInfo(func);
|
|
58
58
|
const path = finfo.file.replace(`${this.appDir}/`, '');
|
|
59
59
|
const suffix = route._controllerName
|
|
60
60
|
? `${route._controllerName}.${route._methodName}`
|
|
@@ -110,13 +110,16 @@ module.exports = class CallContext {
|
|
|
110
110
|
|
|
111
111
|
if (arg && typeof arg === 'object') {
|
|
112
112
|
for (const key in arg) {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
const trackedData = tracker.getData(arg[key]);
|
|
114
|
+
if (trackedData) {
|
|
115
|
+
const { start, stop } = trackedData.tagRanges[0];
|
|
116
|
+
const taintedString = arg[key].substring(start, stop + 1);
|
|
117
|
+
const taintRangeStart = CallContext.valueString(arg).indexOf(taintedString);
|
|
118
|
+
if (taintRangeStart === -1) {
|
|
116
119
|
// If tracked string is not in the abbreviated stringified obj, disable highlighting
|
|
117
120
|
return new TagRange(0, 0, 'disable-highlighting');
|
|
118
121
|
}
|
|
119
|
-
return new TagRange(
|
|
122
|
+
return new TagRange(taintRangeStart, taintRangeStart + taintedString.length - 1, 'untrusted');
|
|
120
123
|
}
|
|
121
124
|
}
|
|
122
125
|
}
|
|
@@ -37,6 +37,7 @@ const KEYS = {
|
|
|
37
37
|
DEFEND: 'defend',
|
|
38
38
|
FASTIFY_HANDLER_RESOLVED: 'fastify.xss.handler.resolved',
|
|
39
39
|
FASTIFY_REPLY_SEND_STATE: 'fastify.xss.reply.send.state',
|
|
40
|
+
FINALHANDLER_CB_INDEX: 'finalHandlerCbIndex',
|
|
40
41
|
HAPI_CALLER: 'hapi.caller',
|
|
41
42
|
INPUT_EXCLUSIONS: 'defend.exclusions',
|
|
42
43
|
KOA_CTX: 'koa.ctx',
|
|
@@ -414,7 +414,7 @@ const instrumentHandler = (layer, id, self, stack) => {
|
|
|
414
414
|
*/
|
|
415
415
|
function getLayerHandleMethod(layer) {
|
|
416
416
|
let methodName = 'handle';
|
|
417
|
-
const __handleData = fnInspect.
|
|
417
|
+
const __handleData = fnInspect.funcInfo(layer.__handle);
|
|
418
418
|
if (__handleData && __handleData.file.includes('express-async-errors')) {
|
|
419
419
|
methodName = '__handle';
|
|
420
420
|
}
|
package/lib/library-usage.js
CHANGED
|
@@ -30,7 +30,7 @@ module.exports.listen = function(evalInterval = 1) {
|
|
|
30
30
|
const handler = (codeEvent) => {
|
|
31
31
|
try {
|
|
32
32
|
if (
|
|
33
|
-
codeEvent.type !== '
|
|
33
|
+
codeEvent.type !== 'LazyCompile' ||
|
|
34
34
|
codeEvent.script.indexOf(`node_modules${path.sep}`) === -1 ||
|
|
35
35
|
reportedFiles.has(codeEvent.script)
|
|
36
36
|
) {
|
package/lib/list-installed.js
CHANGED
|
@@ -12,6 +12,8 @@ Copyright: 2022 Contrast Security, Inc
|
|
|
12
12
|
engineered, modified, repackaged, sold, redistributed or otherwise used in a
|
|
13
13
|
way not consistent with the End User License Agreement.
|
|
14
14
|
*/
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
15
17
|
const semver = require('semver');
|
|
16
18
|
const util = require('util');
|
|
17
19
|
|
|
@@ -19,6 +21,8 @@ const {
|
|
|
19
21
|
AGENT_INFO: { SUPPORTED_NPM_VERSIONS }
|
|
20
22
|
} = require('./constants');
|
|
21
23
|
|
|
24
|
+
const VERSION_REGEX = /^npm@(\S+)\s+(\S+)$/m;
|
|
25
|
+
|
|
22
26
|
const execFile = util.promisify(require('child_process').execFile);
|
|
23
27
|
|
|
24
28
|
/**
|
|
@@ -36,41 +40,61 @@ const execFile = util.promisify(require('child_process').execFile);
|
|
|
36
40
|
*/
|
|
37
41
|
module.exports = async function listInstalled(cwd, logger) {
|
|
38
42
|
const env = { ...process.env, NODE_OPTIONS: undefined };
|
|
39
|
-
const args = ['
|
|
43
|
+
const args = ['ls', '--json', '--prod', '--long'];
|
|
44
|
+
let stdout;
|
|
40
45
|
|
|
41
46
|
try {
|
|
42
|
-
const
|
|
47
|
+
const result = await execFile('npm', ['help'], {
|
|
43
48
|
cwd,
|
|
44
49
|
env,
|
|
45
|
-
shell: true
|
|
50
|
+
shell: true,
|
|
46
51
|
});
|
|
52
|
+
stdout = result.stdout;
|
|
53
|
+
} catch (err) {
|
|
54
|
+
logger.debug('`npm` returned an error: %o', err);
|
|
55
|
+
// If npm encounters any errors whatsoever it will return with a non-zero
|
|
56
|
+
// exit code but still output the relevant information to stdout.
|
|
57
|
+
// If an even worse error occurs, we may not be able to parse stdout.
|
|
58
|
+
stdout = err.stdout || '';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const [, version, location] = stdout.match(VERSION_REGEX) || [];
|
|
62
|
+
if (!version)
|
|
63
|
+
throw new Error(
|
|
64
|
+
'Unable to locate `npm`. Please enable debug level logs for more information.'
|
|
65
|
+
);
|
|
47
66
|
|
|
48
|
-
|
|
49
|
-
logger.debug('using npm version %s', version.trim());
|
|
50
|
-
if (!semver.satisfies(version, SUPPORTED_NPM_VERSIONS))
|
|
51
|
-
logger.warn(
|
|
52
|
-
'the installed version of npm can cause unexpected behavior. please install a version that satisfies %s',
|
|
53
|
-
SUPPORTED_NPM_VERSIONS
|
|
54
|
-
);
|
|
67
|
+
logger.debug('using npm version %s at %s', version, location);
|
|
55
68
|
|
|
56
|
-
|
|
69
|
+
if (semver.gte(version, '7.0.0')) args.push('--all');
|
|
70
|
+
if (!semver.satisfies(version, SUPPORTED_NPM_VERSIONS))
|
|
71
|
+
logger.warn(
|
|
72
|
+
'The installed version of npm (%s at %s) can cause unexpected behavior. Please install a version that satisfies %s',
|
|
73
|
+
version,
|
|
74
|
+
location,
|
|
75
|
+
SUPPORTED_NPM_VERSIONS
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const result = await execFile('npm', args, {
|
|
57
80
|
cwd,
|
|
58
81
|
env,
|
|
59
82
|
shell: true,
|
|
60
|
-
maxBuffer: 1024 * 1024 * 128
|
|
83
|
+
maxBuffer: 1024 * 1024 * 128,
|
|
61
84
|
});
|
|
62
85
|
|
|
63
|
-
|
|
86
|
+
stdout = result.stdout;
|
|
64
87
|
} catch (err) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
88
|
+
logger.debug('`npm ls` returned an error: %o', err);
|
|
89
|
+
stdout = err.stdout || '';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
return JSON.parse(stdout);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
logger.trace('parsing the output of `npm ls` failed: %o', err);
|
|
96
|
+
throw new Error(
|
|
97
|
+
'`npm ls` failed to provide a list of installed dependencies. Please enable debug level logs for more information.'
|
|
98
|
+
);
|
|
75
99
|
}
|
|
76
100
|
};
|
|
@@ -12,12 +12,16 @@ Copyright: 2022 Contrast Security, Inc
|
|
|
12
12
|
engineered, modified, repackaged, sold, redistributed or otherwise used in a
|
|
13
13
|
way not consistent with the End User License Agreement.
|
|
14
14
|
*/
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
15
17
|
const ProtectSink = require('./sinks');
|
|
16
|
-
const
|
|
18
|
+
const ProtectSource = require('./sources');
|
|
19
|
+
const utils = require('./utils');
|
|
17
20
|
|
|
18
21
|
module.exports = class ExpressInstrumentation {
|
|
19
22
|
constructor() {
|
|
23
|
+
utils.install();
|
|
20
24
|
new ProtectSink();
|
|
21
|
-
new
|
|
25
|
+
new ProtectSource();
|
|
22
26
|
}
|
|
23
27
|
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Copyright: 2022 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 moduleHook = require('../../hooks/require');
|
|
19
|
+
const { PATCH_TYPES } = require('../../constants');
|
|
20
|
+
const { AsyncStorage, KEYS } = require('../../core/async-storage');
|
|
21
|
+
|
|
22
|
+
module.exports.install = function () {
|
|
23
|
+
moduleHook.resolve({ name: 'finalhandler' }, (finalhandler) =>
|
|
24
|
+
patcher.patch(finalhandler, {
|
|
25
|
+
name: 'finalHandler',
|
|
26
|
+
patchType: PATCH_TYPES.FRAMEWORK,
|
|
27
|
+
post(data) {
|
|
28
|
+
data.result = patcher.patch(data.result, {
|
|
29
|
+
name: 'finalHandler.returnedFunction',
|
|
30
|
+
patchType: PATCH_TYPES.FRAMEWORK,
|
|
31
|
+
pre(data) {
|
|
32
|
+
const req = AsyncStorage.get(KEYS.REQ);
|
|
33
|
+
|
|
34
|
+
if (!req || !req.__onFinished || !req.__onFinished.queue) {
|
|
35
|
+
data.queueLength = 0;
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
data.queueLength = req.__onFinished.queue.length;
|
|
39
|
+
},
|
|
40
|
+
post(data) {
|
|
41
|
+
if (!('queueLength' in data)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const req = AsyncStorage.get(KEYS.REQ);
|
|
45
|
+
|
|
46
|
+
if (
|
|
47
|
+
req &&
|
|
48
|
+
req.__onFinished &&
|
|
49
|
+
req.__onFinished.queue &&
|
|
50
|
+
req.__onFinished.queue.length &&
|
|
51
|
+
req.__onFinished.queue.length - data.queueLength == 1
|
|
52
|
+
) {
|
|
53
|
+
AsyncStorage.set(KEYS.FINALHANDLER_CB_INDEX, data.queueLength);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
})
|
|
59
|
+
);
|
|
60
|
+
};
|
package/lib/protect/service.js
CHANGED
|
@@ -36,6 +36,8 @@ const headerValidators = require('./validators');
|
|
|
36
36
|
const UserInputKit = require('../reporter/models/utils/user-input-kit');
|
|
37
37
|
const UserInputFactory = require('../reporter/models/utils/user-input-factory');
|
|
38
38
|
const blockRequest = require('../util/block-request');
|
|
39
|
+
const { AsyncStorage, KEYS } = require('../core/async-storage');
|
|
40
|
+
|
|
39
41
|
|
|
40
42
|
const evalOptions = { preferWorthWatching: true };
|
|
41
43
|
|
|
@@ -219,17 +221,32 @@ class ProtectService {
|
|
|
219
221
|
if (!rules) {
|
|
220
222
|
return {};
|
|
221
223
|
}
|
|
222
|
-
// also, if content-type has multipart...
|
|
223
|
-
const bodyBuffer = Buffer.concat(chunks);
|
|
224
224
|
|
|
225
|
-
|
|
225
|
+
let bodyData = '';
|
|
226
|
+
|
|
227
|
+
if (Array.isArray(chunks)) {
|
|
228
|
+
if (typeof chunks[0] == 'string') {
|
|
229
|
+
const bodyStr = ''.concat('', ...chunks);
|
|
230
|
+
bodyData = Buffer.from(bodyStr).toString('base64');
|
|
231
|
+
} else if (Buffer.isBuffer(chunks[0])) {
|
|
232
|
+
const bodyBuffer = Buffer.concat(chunks);
|
|
233
|
+
bodyData = Uint8Array.from(bodyBuffer);
|
|
234
|
+
} else {
|
|
235
|
+
logger.error('Invalid chunk type');
|
|
236
|
+
}
|
|
237
|
+
} else {
|
|
238
|
+
logger.error('Invalid chunk type');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// also, if content-type has multipart...
|
|
242
|
+
const findings = this.agentLib.scoreRequestBody(
|
|
226
243
|
rules,
|
|
227
|
-
|
|
244
|
+
bodyData,
|
|
228
245
|
evalOptions
|
|
229
246
|
);
|
|
230
247
|
|
|
231
248
|
// store body buffer on findings for nosqli sink.
|
|
232
|
-
findings.bodyBuffer =
|
|
249
|
+
findings.bodyBuffer = bodyData;
|
|
233
250
|
return findings;
|
|
234
251
|
}
|
|
235
252
|
|
|
@@ -489,6 +506,11 @@ class ProtectService {
|
|
|
489
506
|
* @returns {Boolean} false which halts executing of original method
|
|
490
507
|
*/
|
|
491
508
|
handleBlockAtPerimeter(res) {
|
|
509
|
+
const finalHandlerCbIndex = AsyncStorage.get(KEYS.FINALHANDLER_CB_INDEX);
|
|
510
|
+
if (finalHandlerCbIndex || finalHandlerCbIndex == 0) {
|
|
511
|
+
const req = AsyncStorage.get(KEYS.REQ);
|
|
512
|
+
req.__onFinished && req.__onFinished.queue && req.__onFinished.queue.splice(finalHandlerCbIndex, 1);
|
|
513
|
+
}
|
|
492
514
|
blockRequest(res);
|
|
493
515
|
// halts further execution of user code
|
|
494
516
|
return false;
|
|
@@ -1135,6 +1157,7 @@ class ProtectService {
|
|
|
1135
1157
|
* @param {Rule[]} rules Rules from which to build findings
|
|
1136
1158
|
* @returns {Object[]} The findings from the rules
|
|
1137
1159
|
*/
|
|
1160
|
+
// eslint-disable-next-line default-param-last
|
|
1138
1161
|
createFindings(rules = [], samples) {
|
|
1139
1162
|
const findings = [];
|
|
1140
1163
|
const speedracer = this.reporter.speedracer &&
|
|
@@ -42,8 +42,14 @@ function RawRequest(data = {}) {
|
|
|
42
42
|
function RawRequestWithBody({ requestId, bodyStr, chunks, buffer }) {
|
|
43
43
|
let _body = '';
|
|
44
44
|
|
|
45
|
-
if (chunks) {
|
|
46
|
-
|
|
45
|
+
if (Array.isArray(chunks)) {
|
|
46
|
+
if (typeof chunks[0] == 'string') {
|
|
47
|
+
const bStr = ''.concat('', ...chunks);
|
|
48
|
+
_body = Buffer.from(bStr).toString('base64');
|
|
49
|
+
} else {
|
|
50
|
+
const bBuffer = Buffer.concat(chunks);
|
|
51
|
+
_body = Uint8Array.from(bBuffer);
|
|
52
|
+
}
|
|
47
53
|
} else if (buffer) {
|
|
48
54
|
_body = Uint8Array.from(buffer);
|
|
49
55
|
} else if (bodyStr) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/agent",
|
|
3
|
-
"version": "4.19.
|
|
3
|
+
"version": "4.19.6",
|
|
4
4
|
"description": "Node.js security instrumentation by Contrast Security",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"security",
|
|
@@ -76,12 +76,12 @@
|
|
|
76
76
|
"@babel/template": "^7.10.4",
|
|
77
77
|
"@babel/traverse": "^7.12.1",
|
|
78
78
|
"@babel/types": "^7.12.1",
|
|
79
|
-
"@contrast/agent-lib": "^4.
|
|
80
|
-
"@contrast/distringuish-prebuilt": "^
|
|
79
|
+
"@contrast/agent-lib": "^4.2.0",
|
|
80
|
+
"@contrast/distringuish-prebuilt": "^3.0.1",
|
|
81
81
|
"@contrast/flat": "^4.1.1",
|
|
82
|
-
"@contrast/fn-inspect": "^
|
|
82
|
+
"@contrast/fn-inspect": "^3.0.0",
|
|
83
83
|
"@contrast/heapdump": "^1.1.0",
|
|
84
|
-
"@contrast/protobuf-api": "^3.2.
|
|
84
|
+
"@contrast/protobuf-api": "^3.2.5",
|
|
85
85
|
"@contrast/require-hook": "^3.0.0",
|
|
86
86
|
"@contrast/synchronous-source-maps": "^1.1.0",
|
|
87
87
|
"amqp-connection-manager": "^3.2.2",
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
"@contrast/screener-service": "^1.12.9",
|
|
122
122
|
"@hapi/boom": "file:test/mock/boom",
|
|
123
123
|
"@hapi/hapi": "file:test/mock/hapi",
|
|
124
|
-
"@ls-lint/ls-lint": "^1.
|
|
124
|
+
"@ls-lint/ls-lint": "^1.11.2",
|
|
125
125
|
"@typescript-eslint/eslint-plugin": "^5.12.1",
|
|
126
126
|
"@typescript-eslint/parser": "^5.12.1",
|
|
127
127
|
"ajv": "^8.5.0",
|