@contrast/agent 4.19.4 → 4.19.7

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.
@@ -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 { funcinfo } = require('@contrast/fn-inspect');
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 = funcinfo(handler);
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 { funcinfo } = require('@contrast/fn-inspect');
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 = funcinfo(func);
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
- if (tracker.getData(arg[key])) {
114
- const start = CallContext.valueString(arg).indexOf(arg[key]);
115
- if (start === -1) {
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(start, start + arg[key].length - 1, 'untrusted');
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.funcinfo(layer.__handle);
417
+ const __handleData = fnInspect.funcInfo(layer.__handle);
418
418
  if (__handleData && __handleData.file.includes('express-async-errors')) {
419
419
  methodName = '__handle';
420
420
  }
@@ -30,7 +30,7 @@ module.exports.listen = function(evalInterval = 1) {
30
30
  const handler = (codeEvent) => {
31
31
  try {
32
32
  if (
33
- codeEvent.type !== 'LAZY_COMPILE' ||
33
+ codeEvent.type !== 'LazyCompile' ||
34
34
  codeEvent.script.indexOf(`node_modules${path.sep}`) === -1 ||
35
35
  reportedFiles.has(codeEvent.script)
36
36
  ) {
@@ -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 = ['--silent', 'ls', '--json', '--prod', '--long'];
43
+ const args = ['ls', '--json', '--prod', '--long'];
44
+ let stdout;
40
45
 
41
46
  try {
42
- const { stdout: version } = await execFile('npm', ['--version'], {
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
- if (semver.gte(version, '7.0.0')) args.push('--all');
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
- const { stdout: list } = await execFile('npm', args, {
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
- return JSON.parse(list);
86
+ stdout = result.stdout;
64
87
  } catch (err) {
65
- // If app has unmet dependencies the command above will throw an ELSPROBLEMS
66
- // error but still output all the dependencies it finds to stdout.
67
- // If an even worse error occurs, we may not be able to parse stdout.
68
- try {
69
- logger.trace('`npm ls` returned an error: %o', err);
70
- return JSON.parse(err.stdout);
71
- } catch (parseErr) {
72
- logger.trace('parsing the output of `npm ls` failed. %o', parseErr);
73
- throw err;
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 ProectSource = require('./sources');
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 ProectSource();
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
+ };
@@ -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
- const findings = this.agentLib.scoreRequestUnknownBody(
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
- bodyBuffer,
244
+ bodyData,
228
245
  evalOptions
229
246
  );
230
247
 
231
248
  // store body buffer on findings for nosqli sink.
232
- findings.bodyBuffer = 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
- _body = Uint8Array.from(Buffer.concat(chunks));
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.4",
3
+ "version": "4.19.7",
4
4
  "description": "Node.js security instrumentation by Contrast Security",
5
5
  "keywords": [
6
6
  "security",
@@ -76,10 +76,10 @@
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.0.0",
80
- "@contrast/distringuish-prebuilt": "^2.2.0",
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": "^2.4.4",
82
+ "@contrast/fn-inspect": "^3.0.0",
83
83
  "@contrast/heapdump": "^1.1.0",
84
84
  "@contrast/protobuf-api": "^3.2.5",
85
85
  "@contrast/require-hook": "^3.0.0",
@@ -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.8.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",
@@ -133,7 +133,6 @@
133
133
  "chai": "^4.2.0",
134
134
  "chai-as-promised": "^7.1.1",
135
135
  "chai-like": "^1.1.1",
136
- "codecov": "^3.7.0",
137
136
  "config": "^3.3.3",
138
137
  "csv-writer": "^1.2.0",
139
138
  "deasync": "^0.1.24",