@contrast/protect 1.14.0 → 1.15.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/lib/index.d.ts CHANGED
@@ -42,10 +42,25 @@ export interface ConnectInputs {
42
42
  queries?: string
43
43
  }
44
44
 
45
+ export interface ExclusionPolicy {
46
+ exclusions: {
47
+ url: [],
48
+ querystring: [],
49
+ header: [],
50
+ body: [],
51
+ cookie: [],
52
+ parameter: []
53
+ }
54
+ }
55
+
56
+ export type ProtectPolicy = ExclusionPolicy & Record<rule, ProtectRuleMode> & { rulesMask: number };
57
+
45
58
  export interface Protect {
46
59
  makeResponseBlocker: (res: ServerResponse) => Block,
47
60
  makeSourceContext: (req: IncomingMessage, res: ServerResponse) => ProtectRequestStore,
48
61
  throwSecurityException: (sourceContext: ProtectRequestStore) => void,
62
+ policy: ProtectPolicy,
63
+ getPolicy(): ProtectPolicy, // creates copy for request scope
49
64
  inputAnalysis: {
50
65
  handleConnect: (sourceContext: ProtectRequestStore, connectInputs: ConnectInputs) => undefined | [string, string],
51
66
  handleRequestEnd: (sourceContext: ProtectRequestStore) => void, //NYI
@@ -15,16 +15,16 @@
15
15
 
16
16
  'use strict';
17
17
 
18
+ const address = require('ipaddr.js');
18
19
  const {
19
20
  BLOCKING_MODES,
20
- traverseKeysAndValues,
21
- traverseValues,
22
21
  Rule,
23
- ProtectRuleMode,
22
+ ProtectRuleMode: { OFF, MONITOR },
24
23
  isString,
25
- ProtectRuleMode: { OFF },
24
+ traverseKeysAndValues,
25
+ traverseValues,
26
+ InputType,
26
27
  } = require('@contrast/common');
27
- const address = require('ipaddr.js');
28
28
 
29
29
  //
30
30
  // these rules are not implemented by agent-lib, but are being considered for
@@ -55,6 +55,40 @@ const agentLibRuleTypeToName = {
55
55
 
56
56
  const preferWW = { preferWorthWatching: true };
57
57
 
58
+ const acceptedMethods = new Set([
59
+ 'acl',
60
+ 'baseline-control',
61
+ 'checkin',
62
+ 'checkout',
63
+ 'connect',
64
+ 'copy',
65
+ 'delete',
66
+ 'get',
67
+ 'head',
68
+ 'label',
69
+ 'lock',
70
+ 'merge',
71
+ 'mkactivity',
72
+ 'mkcalendar',
73
+ 'mkcol',
74
+ 'mkworkspace',
75
+ 'move',
76
+ 'options',
77
+ 'orderpatch',
78
+ 'patch',
79
+ 'post',
80
+ 'propfind',
81
+ 'proppatch',
82
+ 'put',
83
+ 'report',
84
+ 'search',
85
+ 'trace',
86
+ 'uncheckout',
87
+ 'unlock',
88
+ 'update',
89
+ 'version-control',
90
+ ]);
91
+
58
92
  module.exports = function(core) {
59
93
  const {
60
94
  logger,
@@ -130,16 +164,22 @@ module.exports = function(core) {
130
164
  inputAnalysis.handleConnect = function handleConnect(sourceContext, connectInputs) {
131
165
  const { policy: { rulesMask } } = sourceContext;
132
166
 
133
- inputAnalysis.handleVirtualPatches(sourceContext, { URLS: connectInputs.rawUrl, HEADERS: connectInputs.headers });
167
+ inputAnalysis.handleVirtualPatches(
168
+ sourceContext,
169
+ { URLS: connectInputs.rawUrl, HEADERS: connectInputs.headers }
170
+ );
134
171
 
135
172
  // initialize findings to the basics
136
173
  let block = undefined;
137
174
  if (rulesMask !== 0) {
138
175
  const findings = agentLib.scoreRequestConnect(rulesMask, connectInputs, preferWW);
139
-
140
176
  block = mergeFindings(sourceContext, findings);
141
177
  }
142
178
 
179
+ if (!block) {
180
+ block = inputAnalysis.handleMethodTampering(sourceContext, connectInputs);
181
+ }
182
+
143
183
  return block;
144
184
  };
145
185
 
@@ -426,19 +466,54 @@ module.exports = function(core) {
426
466
  }
427
467
  };
428
468
 
469
+ inputAnalysis.handleMethodTampering = function(sourceContext, connectInputs) {
470
+ const ruleId = Rule.METHOD_TAMPERING;
471
+ const mode = sourceContext.policy[ruleId];
472
+ if (mode !== OFF) {
473
+ const { method } = connectInputs;
474
+
475
+ if (!acceptedMethods.has(method)) {
476
+ const result = {
477
+ inputType: InputType.METHOD,
478
+ key: 'method',
479
+ value: method,
480
+ blocked: false,
481
+ exploitMetadata: null,
482
+ };
483
+
484
+ sourceContext.resultsMap[ruleId] = [result];
485
+
486
+ if (BLOCKING_MODES.includes(mode)) {
487
+ result.blocked = true;
488
+ return sourceContext.securityException = ['block', ruleId];
489
+ }
490
+ }
491
+ }
492
+ };
493
+
429
494
  /**
430
- * handleRequestEnd()
431
495
  *
432
- * Invoked when the request is complete.
496
+ * Invoked when the request is complete. Handles probe analysis (when configured) and
497
+ * various other tasks needed prior to reporting.
433
498
  *
434
499
  * @param {Object} sourceContext
435
500
  */
436
501
  inputAnalysis.handleRequestEnd = function handleRequestEnd(sourceContext) {
502
+ {
503
+ // check status code to verify method-tampering exploitation
504
+ const mtResult = sourceContext.resultsMap[Rule.METHOD_TAMPERING]?.[0];
505
+ if (mtResult) {
506
+ const { statusCode } = sourceContext.resData;
507
+ if (statusCode !== 405 || statusCode !== 501) {
508
+ mtResult.exploitMetadata = [{ statusCode }];
509
+ }
510
+ }
511
+ }
512
+
437
513
  if (!config.protect.probe_analysis.enable || sourceContext.allowed) return;
438
514
 
439
515
  // Detecting probes
440
516
  const { resultsMap, policy: { rulesMask } } = sourceContext;
441
-
442
517
  const probesRules = [Rule.CMD_INJECTION, Rule.PATH_TRAVERSAL, Rule.SQL_INJECTION, Rule.XXE];
443
518
  const probes = {};
444
519
 
@@ -868,5 +943,5 @@ function getValueAtKey(obj, path, key) {
868
943
  }
869
944
 
870
945
  function isMonitorMode(ruleId, sourceContext) {
871
- return sourceContext.policy[ruleId] === ProtectRuleMode.MONITOR;
946
+ return sourceContext.policy[ruleId] === MONITOR;
872
947
  }
@@ -15,7 +15,7 @@
15
15
 
16
16
  'use strict';
17
17
 
18
- const { Event } = require('@contrast/common');
18
+ const { Event, toLowerCase } = require('@contrast/common');
19
19
  const { patchType } = require('../constants');
20
20
 
21
21
  module.exports = function(core) {
@@ -62,10 +62,12 @@ module.exports = function(core) {
62
62
 
63
63
  store.protect = core.protect.makeSourceContext(req, res);
64
64
  const {
65
- reqData: { headers, uriPath, method }
65
+ reqData: { headers, uriPath, method },
66
+ resData,
66
67
  } = store.protect;
67
68
 
68
69
  res.on('finish', () => {
70
+ resData.statusCode = res.statusCode;
69
71
  inputAnalysis.handleRequestEnd(store.protect);
70
72
  messages.emit(Event.PROTECT, store);
71
73
  });
@@ -73,7 +75,7 @@ module.exports = function(core) {
73
75
  const connectInputs = {
74
76
  headers: removeCookies(headers),
75
77
  uriPath,
76
- method
78
+ method: toLowerCase(method),
77
79
  };
78
80
 
79
81
  if (inputAnalysis.virtualPatchesEvaluators?.length) {
@@ -25,17 +25,18 @@ module.exports = function(core) {
25
25
 
26
26
  // instrumentation
27
27
  require('./install/child-process')(core);
28
+ require('./install/eval')(core);
28
29
  require('./install/fs')(core);
29
- require('./install/mongodb')(core);
30
+ require('./install/function')(core);
31
+ require('./install/http')(core);
30
32
  require('./install/marsdb')(core);
33
+ require('./install/mongodb')(core);
34
+ require('./install/mssql')(core);
31
35
  require('./install/mysql')(core);
32
36
  require('./install/postgres')(core);
33
37
  require('./install/sequelize')(core);
34
38
  require('./install/sqlite3')(core);
35
- require('./install/http')(core);
36
39
  require('./install/vm')(core);
37
- require('./install/eval')(core);
38
- require('./install/function')(core);
39
40
  // TODO: NODE-2360 (oracledb)
40
41
 
41
42
  inputTracing.install = function() {
@@ -80,6 +80,10 @@ module.exports = function(core) {
80
80
  depHooks.resolve({ name: 'fs' }, fs => {
81
81
  fsMethods.forEach(({ method, indices = [0] }) => {
82
82
  const name = `fs.${method}`;
83
+
84
+ // may not exist depending on OS
85
+ if (!fs[method]) return;
86
+
83
87
  patcher.patch(fs, method, {
84
88
  name,
85
89
  patchType,
@@ -0,0 +1,79 @@
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 { isString } = require('@contrast/common');
19
+ const { patchType } = require('../constants');
20
+
21
+ module.exports = function (core) {
22
+ const {
23
+ depHooks,
24
+ patcher,
25
+ protect,
26
+ protect: { inputTracing },
27
+ } = core;
28
+
29
+ const pre = ({ args, hooked, name, orig }) => {
30
+ const sourceContext = protect.getSourceContext(name);
31
+ const [value] = args;
32
+ if (!sourceContext || !value || !isString(value)) return;
33
+
34
+ const sinkContext = {
35
+ name,
36
+ value,
37
+ stacktraceOpts: {
38
+ constructorOpt: hooked,
39
+ prependFrames: [orig],
40
+ },
41
+ };
42
+
43
+ inputTracing.handleSqlInjection(sourceContext, sinkContext);
44
+ };
45
+
46
+ core.protect.inputTracing.mssqlInstrumentation = {
47
+ install() {
48
+ depHooks.resolve(
49
+ { name: 'mssql', file: 'lib/base/prepared-statement.js' },
50
+ (PreparedStatement) => {
51
+ patcher.patch(PreparedStatement.prototype, 'prepare', {
52
+ name: 'mssql.PreparedStatement.prototype.prepare',
53
+ patchType,
54
+ pre,
55
+ });
56
+ },
57
+ );
58
+
59
+ depHooks.resolve(
60
+ { name: 'mssql', file: 'lib/base/request.js' },
61
+ (Request) => {
62
+ patcher.patch(Request.prototype, 'batch', {
63
+ name: 'mssql.Request.prototype.batch',
64
+ patchType,
65
+ pre,
66
+ });
67
+
68
+ patcher.patch(Request.prototype, 'query', {
69
+ name: 'mssql.Request.prototype.query',
70
+ patchType,
71
+ pre,
72
+ });
73
+ },
74
+ );
75
+ },
76
+ };
77
+
78
+ return core.protect.inputTracing.mssqlInstrumentation;
79
+ };
@@ -74,19 +74,16 @@ module.exports = function(core) {
74
74
  contentType,
75
75
  };
76
76
 
77
- //
78
- // build the protect object that contains all
79
- //
80
77
  const protectStore = {
81
78
  reqData,
79
+ resData: {
80
+ statusCode: null,
81
+ },
82
82
  // block closure captures res so it isn't exposed to beyond here
83
83
  block: core.protect.makeResponseBlocker(res),
84
-
85
84
  policy,
86
-
87
85
  exclusions: [],
88
86
  virtualPatchesEvaluators: [],
89
-
90
87
  trackRequest: false,
91
88
  securityException: undefined,
92
89
  // bodyType is set to a body type if handlers.parseRawBody() parsed it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/protect",
3
- "version": "1.14.0",
3
+ "version": "1.15.1",
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)",
@@ -18,9 +18,9 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@contrast/agent-lib": "^5.3.4",
21
- "@contrast/common": "1.5.0",
22
- "@contrast/core": "1.12.0",
23
- "@contrast/esm-hooks": "1.8.0",
21
+ "@contrast/common": "1.6.0",
22
+ "@contrast/core": "1.13.1",
23
+ "@contrast/esm-hooks": "1.9.1",
24
24
  "@contrast/scopes": "1.3.0",
25
25
  "ipaddr.js": "^2.0.1",
26
26
  "semver": "^7.3.7"