@contrast/protect 1.12.2 → 1.13.0

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.
@@ -0,0 +1,40 @@
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
+
16
+ 'use strict';
17
+
18
+ const { isSecurityException } = require('../security-exception');
19
+
20
+ module.exports = function(core) {
21
+ const {
22
+ logger,
23
+ protect: { getSourceContext },
24
+ } = core;
25
+
26
+ return core.protect.errorHandlers.commonHandler = function(err) {
27
+ if (!isSecurityException(err)) {
28
+ throw err;
29
+ }
30
+
31
+ const sourceContext = getSourceContext('protect-common-error-handler');
32
+ if (!sourceContext) {
33
+ logger.info('SecurityException caught by Contrast but Protect store is unavailable for req handling');
34
+ return;
35
+ }
36
+
37
+ const blockInfo = sourceContext.securityException;
38
+ sourceContext.block(...blockInfo);
39
+ };
40
+ };
@@ -15,20 +15,23 @@
15
15
 
16
16
  'use strict';
17
17
 
18
+ const { callChildComponentMethodsSync } = require('@contrast/common');
19
+
18
20
  module.exports = function(core) {
19
21
  const errorHandlers = core.protect.errorHandlers = {};
20
22
 
23
+ // api
24
+ require('./common-handler')(core);
25
+ require('./init-domain')(core);
26
+
27
+ // installers
21
28
  require('./install/fastify')(core);
22
29
  require('./install/koa2')(core);
23
30
  require('./install/express4')(core);
24
31
  require('./install/hapi')(core);
25
32
 
26
33
  errorHandlers.install = function() {
27
- for (const component of Object.values(errorHandlers)) {
28
- if (component.install) {
29
- component.install();
30
- }
31
- }
34
+ callChildComponentMethodsSync(errorHandlers, 'install');
32
35
  };
33
36
 
34
37
  return errorHandlers;
@@ -0,0 +1,61 @@
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
+
16
+ 'use strict';
17
+
18
+ const process = require('process');
19
+ const semver = require('semver');
20
+
21
+ module.exports = function(core) {
22
+ const {
23
+ logger,
24
+ protect: { errorHandlers }
25
+ } = core;
26
+
27
+ Object.assign(errorHandlers, initSupportedDomainPackage());
28
+
29
+ return errorHandlers.initDomain = function(req, res) {
30
+ const { AsyncHookDomain, Domain } = errorHandlers;
31
+
32
+ if (AsyncHookDomain) {
33
+ new AsyncHookDomain(errorHandlers.commonHandler);
34
+ } else {
35
+ const domain = new Domain();
36
+ domain.add(req);
37
+ domain.add(res);
38
+ domain.on('error', errorHandlers.commonHandler);
39
+ return domain;
40
+ }
41
+ };
42
+
43
+ function initSupportedDomainPackage() {
44
+ let AsyncHookDomain, Domain;
45
+
46
+ if (semver.lt(process.version, '16.0.0')) {
47
+ logger.info(
48
+ '%s. %s. %s.',
49
+ 'falling back to deprecated \'domain\' module for async SecurityException handling',
50
+ 'upgrade to Node 16 LTS or above to allow use of \'async-hook-domain\' modern alternative',
51
+ 'upgrading will resolve any deprecation warnings and prevent the logging of this message'
52
+ );
53
+
54
+ Domain = require('domain').Domain;
55
+ } else {
56
+ AsyncHookDomain = require('async-hook-domain');
57
+ }
58
+
59
+ return { AsyncHookDomain, Domain };
60
+ }
61
+ };
package/lib/index.d.ts CHANGED
@@ -13,14 +13,11 @@
13
13
  * way not consistent with the End User License Agreement.
14
14
  */
15
15
 
16
- import { Logger } from '@contrast/logger';
17
- import { Sources } from '@contrast/scopes';
18
- import RequireHook from '@contrast/require-hook';
19
- import { RulesConfig, Messages, ReqData, ProtectMessage, ResultMap, ProtectRuleMode } from '@contrast/common';
16
+ import { ReqData, ProtectMessage, ResultMap, ProtectRuleMode } from '@contrast/common';
20
17
  import { IncomingMessage, ServerResponse } from 'node:http';
21
- import { Config } from '@contrast/config';
22
18
  import * as http from 'node:http';
23
19
  import * as https from 'node:https';
20
+ import { Domain } from 'domain';
24
21
 
25
22
  type Http = typeof http;
26
23
  type Https = typeof https;
@@ -29,11 +26,7 @@ export type Block = (mode: string, ruleId: string) => void;
29
26
  export interface ProtectRequestStore {
30
27
  reqData: ReqData;
31
28
  block: Block;
32
- rules: {
33
- agentLibRules: RulesConfig;
34
- agentLibRulesMask: number;
35
- agentRules: RulesConfig;
36
- };
29
+ rules: Record<Rule, { mode: ProtectRuleMode }>;
37
30
  exclusions: any[]; // TODO
38
31
  virtualPatches: any[]; // TODO
39
32
  trackRequest: boolean;
@@ -105,6 +98,8 @@ export interface Protect {
105
98
  install: () => void
106
99
  }
107
100
  errorHandlers: {
101
+ commonHandler: (err: Error) => void;
102
+ initDomain: () => void | Domain;
108
103
  fastify3ErrorHandler: {
109
104
  _userHandler: null | ((...args: any[]) => any),
110
105
  defaultErrorHandler: (error: Error, request: IncomingMessage, reply: ServerResponse) => void,
package/lib/index.js CHANGED
@@ -28,11 +28,11 @@ module.exports = function(core) {
28
28
  require('./make-response-blocker')(core);
29
29
  require('./make-source-context')(core);
30
30
  require('./get-source-context')(core);
31
+ require('./error-handlers')(core);
31
32
  require('./input-analysis')(core);
32
33
  require('./input-tracing')(core);
33
34
  require('./hardening')(core);
34
35
  require('./semantic-analysis')(core);
35
- require('./error-handlers')(core);
36
36
 
37
37
  protect.install = function() {
38
38
  callChildComponentMethodsSync(protect, 'install');
@@ -15,8 +15,8 @@
15
15
 
16
16
  'use strict';
17
17
 
18
- const { patchType } = require('../constants');
19
18
  const { Event } = require('@contrast/common');
19
+ const { patchType } = require('../constants');
20
20
 
21
21
  module.exports = function(core) {
22
22
  const {
@@ -24,9 +24,17 @@ module.exports = function(core) {
24
24
  messages,
25
25
  scopes: { sources },
26
26
  instrumentation: { instrument },
27
- protect: { inputAnalysis },
27
+ protect: {
28
+ inputAnalysis,
29
+ errorHandlers: { initDomain }
30
+ },
28
31
  } = core;
29
32
 
33
+ const instr = inputAnalysis.httpInstrumentation = {
34
+ install,
35
+ around
36
+ };
37
+
30
38
  function removeCookies(headers) {
31
39
  for (let i = 0; i < headers.length; i += 2) {
32
40
  if (headers[i] === 'cookies') {
@@ -48,7 +56,7 @@ module.exports = function(core) {
48
56
  try {
49
57
  store = sources.getStore();
50
58
  if (!store) {
51
- logger.debug('cannot acquire store for around()');
59
+ logger.debug('request store not available during http input-analysis');
52
60
  return;
53
61
  }
54
62
 
@@ -83,11 +91,21 @@ module.exports = function(core) {
83
91
 
84
92
  block = block || inputAnalysis.handleConnect(store.protect, connectInputs);
85
93
  } catch (err) {
86
- logger.error({ err }, 'Error during input analysis');
94
+ logger.error({ err }, 'Error during http input analysis');
87
95
  }
88
96
 
89
97
  if (!block) {
90
- setImmediate(() => next.call(data.obj, ...data.args));
98
+ setImmediate(() => {
99
+ const domain = initDomain(req, res);
100
+
101
+ if (domain) {
102
+ domain.run(() => {
103
+ next.call(data.obj, ...data.args);
104
+ });
105
+ } else {
106
+ next.call(data.obj, ...data.args);
107
+ }
108
+ });
91
109
  } else {
92
110
  store.protect.block(...block);
93
111
  logger.debug({ block }, 'request blocked by not emitting request event');
@@ -129,6 +147,7 @@ module.exports = function(core) {
129
147
  around
130
148
  }
131
149
  ];
150
+
132
151
  instrument({
133
152
  moduleName,
134
153
  patchObjects
@@ -136,8 +155,5 @@ module.exports = function(core) {
136
155
  });
137
156
  }
138
157
 
139
- return inputAnalysis.httpInstrumentation = {
140
- install,
141
- around
142
- };
158
+ return instr;
143
159
  };
@@ -73,13 +73,16 @@ module.exports = function(core) {
73
73
 
74
74
  for (const result of results) {
75
75
  let findings = null;
76
- const inputIndex = sinkContext.value.indexOf(result.value);
77
- if (inputIndex !== -1) {
76
+ let inputIndex = sinkContext.value.indexOf(result.value);
77
+
78
+ while (!findings && inputIndex >= 0) {
78
79
  findings = agentLib.checkCommandInjectionSink(
79
80
  inputIndex,
80
81
  result.value.length,
81
82
  sinkContext.value,
82
83
  );
84
+
85
+ inputIndex = sinkContext.value.indexOf(result.value, inputIndex + 1);
83
86
  }
84
87
 
85
88
  if (findings) {
@@ -96,28 +99,26 @@ module.exports = function(core) {
96
99
 
97
100
  for (const result of results) {
98
101
  let findings = null;
102
+ let inputIndex = sinkContext.value.indexOf(result.value);
99
103
 
100
- const inputIndex = sinkContext.value.indexOf(result.value);
101
-
102
- // if the user input is not in the sink input, there is nothing to do.
103
- if (inputIndex === -1) {
104
- continue;
105
- }
104
+ while (!findings && inputIndex >= 0) {
105
+ if (inputIndex === 0 && sinkContext.value === result.value) {
106
+ findings = {
107
+ startIndex: 0,
108
+ endIndex: result.value.length - 1,
109
+ overrunIndex: 0,
110
+ boundaryIndex: 0,
111
+ };
112
+ } else {
113
+ findings = agentLib.checkSqlInjectionSink(
114
+ inputIndex,
115
+ result.value.length,
116
+ 2,
117
+ sinkContext.value,
118
+ );
119
+ }
106
120
 
107
- if (inputIndex === 0 && sinkContext.value === result.value) {
108
- findings = {
109
- startIndex: 0,
110
- endIndex: result.value.length - 1,
111
- overrunIndex: 0,
112
- boundaryIndex: 0,
113
- };
114
- } else {
115
- findings = agentLib.checkSqlInjectionSink(
116
- inputIndex,
117
- result.value.length,
118
- 2,
119
- sinkContext.value,
120
- );
121
+ inputIndex = sinkContext.value.indexOf(result.value, inputIndex + 1);
121
122
  }
122
123
 
123
124
  if (findings) {
@@ -210,25 +211,27 @@ module.exports = function(core) {
210
211
  for (const v of sinkValuesArr) {
211
212
  if (findings) break;
212
213
 
213
- const inputIndex = v.indexOf(result.value);
214
+ let inputIndex = v.indexOf(result.value);
214
215
 
215
- if (inputIndex === 0 && v === result.value) {
216
- findings = {
217
- startIndex: 0,
218
- endIndex: result?.value.length - 1,
219
- boundaryIndex: 0,
220
- codeString: result.value
221
- };
222
- }
216
+ while (!findings && inputIndex >= 0) {
217
+ if (inputIndex === 0 && v === result.value) {
218
+ findings = {
219
+ startIndex: 0,
220
+ endIndex: result?.value.length - 1,
221
+ boundaryIndex: 0,
222
+ codeString: result.value
223
+ };
224
+ } else {
225
+ const endIndex = inputIndex + result?.value.length;
226
+ findings = agentLib.checkSsjsInjectionSink(v, inputIndex, result.value.length) && {
227
+ startIndex: inputIndex,
228
+ endIndex,
229
+ boundaryIndex: inputIndex,
230
+ codeString: result.value
231
+ };
232
+ }
223
233
 
224
- if (inputIndex > 0) {
225
- const endIndex = inputIndex + result?.value.length;
226
- findings = agentLib.checkSsjsInjectionSink(v, inputIndex, endIndex) && {
227
- startIndex: inputIndex,
228
- endIndex,
229
- boundaryIndex: inputIndex,
230
- codeString: result.value
231
- };
234
+ inputIndex = v.indexOf(result.value, inputIndex + 1);
232
235
  }
233
236
  }
234
237
 
@@ -308,34 +311,30 @@ function handleStringValue(result, cmd, agentLib) {
308
311
  }
309
312
 
310
313
  let findings = null;
311
- let inputIndex = -1;
312
- inputIndex = cmd.indexOf(result.value);
314
+ let inputIndex = cmd.indexOf(result.value);
315
+
316
+ while (!findings && inputIndex >= 0) {
317
+ if (inputIndex === 0 && cmd === result.value) {
318
+ findings = {
319
+ start: 0,
320
+ end: result.value.length - 1,
321
+ boundaryOverrunIndex: 0,
322
+ inputBoundaryIndex: 0,
323
+ };
324
+ } else {
325
+ const isAttack = agentLib.checkSsjsInjectionSink(cmd, inputIndex, result.value.length);
313
326
 
314
- // if the user input is not in the sink input, there is nothing to do.
315
- if (inputIndex === -1) {
316
- return findings;
317
- }
327
+ if (isAttack) {
328
+ findings = {
329
+ start: inputIndex,
330
+ end: inputIndex + result.value.length - 1,
331
+ boundaryOverrunIndex: 0,
332
+ inputBoundaryIndex: 0,
333
+ };
334
+ }
335
+ }
318
336
 
319
- if (inputIndex === 0 && cmd === result.value) {
320
- findings = {
321
- start: 0,
322
- end: result.value.length - 1,
323
- boundaryOverrunIndex: 0,
324
- inputBoundaryIndex: 0,
325
- };
326
- } else {
327
- // This is a temporary workaround, while `agent-lib` fixes
328
- // the `checkSsjsInjectionSink` so it can detect the "TRUE-CLAUSE-1" correctly
329
- // TODO: NODE-2897
330
- const isAttack = result.idsList.includes('TRUE-CLAUSE-1') || agentLib.checkSsjsInjectionSink(cmd, inputIndex, result.value.length);
331
- if (!isAttack) return findings;
332
-
333
- findings = {
334
- start: inputIndex,
335
- end: inputIndex + result.value.length - 1,
336
- boundaryOverrunIndex: 0,
337
- inputBoundaryIndex: 0,
338
- };
337
+ inputIndex = cmd.indexOf(result.value, inputIndex + 1);
339
338
  }
340
339
 
341
340
  return findings;
@@ -32,12 +32,17 @@ module.exports = function(core) {
32
32
  if (isString(value)) {
33
33
  return value;
34
34
  }
35
+
36
+ if (isString(value.sql)) {
37
+ return value.sql;
38
+ }
35
39
  };
36
40
 
37
41
  mysqlInstr.install = function() {
38
42
  [
39
43
  { module: 'mysql', file: 'lib/Connection.js', method: 'query' },
40
- { module: 'mysql2', file: 'lib/Connection.js', method: 'execute' }
44
+ { module: 'mysql2', file: 'lib/connection.js', method: 'execute' },
45
+ { module: 'mysql2', file: 'lib/connection.js', method: 'query' }
41
46
  ].forEach(
42
47
  ({ module, file, method }) => {
43
48
  depHooks.resolve({ module, file, method }, conn => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/protect",
3
- "version": "1.12.2",
3
+ "version": "1.13.0",
4
4
  "description": "Contrast service providing framework-agnostic Protect support",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",
@@ -17,12 +17,15 @@
17
17
  "test": "../scripts/test.sh"
18
18
  },
19
19
  "dependencies": {
20
- "@contrast/agent-lib": "^5.3.1",
21
- "@contrast/common": "1.3.2",
22
- "@contrast/core": "1.10.2",
23
- "@contrast/esm-hooks": "1.6.2",
24
- "@contrast/scopes": "1.2.0",
20
+ "@contrast/agent-lib": "^5.3.4",
21
+ "@contrast/common": "1.4.0",
22
+ "@contrast/core": "1.11.0",
23
+ "@contrast/esm-hooks": "1.7.0",
24
+ "@contrast/scopes": "1.3.0",
25
25
  "ipaddr.js": "^2.0.1",
26
26
  "semver": "^7.3.7"
27
+ },
28
+ "optionalDependencies": {
29
+ "async-hook-domain": "^3.0.2"
27
30
  }
28
31
  }