@contrast/protect 1.17.0 → 1.19.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.
@@ -169,12 +169,8 @@ module.exports = function(core) {
169
169
  { URLS: connectInputs.rawUrl, HEADERS: connectInputs.headers }
170
170
  );
171
171
 
172
- // initialize findings to the basics
173
- let block = undefined;
174
- if (rulesMask !== 0) {
175
- const findings = agentLib.scoreRequestConnect(rulesMask, connectInputs, preferWW);
176
- block = mergeFindings(sourceContext, findings);
177
- }
172
+ const findings = agentLib.scoreRequestConnect(rulesMask, connectInputs, preferWW);
173
+ let block = mergeFindings(sourceContext, findings);
178
174
 
179
175
  if (!block) {
180
176
  block = inputAnalysis.handleMethodTampering(sourceContext, connectInputs);
@@ -510,7 +506,7 @@ module.exports = function(core) {
510
506
  }
511
507
  }
512
508
 
513
- if (!config.protect.probe_analysis.enable || sourceContext.allowed) return;
509
+ if (!config.protect.probe_analysis.enable) return;
514
510
 
515
511
  // Detecting probes
516
512
  const { resultsMap, policy: { rulesMask } } = sourceContext;
@@ -696,7 +692,7 @@ module.exports = function(core) {
696
692
  key: type === 'Key' ? value : path[path.length - 1],
697
693
  value,
698
694
  score: item.score,
699
- idsList: [],
695
+ idsList: item.idsList || [],
700
696
  };
701
697
  if (item.mongoContext) {
702
698
  result.mongoContext = item.mongoContext;
@@ -49,6 +49,20 @@ module.exports = function(core) {
49
49
  let store, block;
50
50
  const { args: [type, req, res] } = data;
51
51
 
52
+ function callNext() {
53
+ setImmediate(() => {
54
+ const domain = initDomain(req, res);
55
+
56
+ if (domain) {
57
+ domain.run(() => {
58
+ next.call(data.obj, ...data.args);
59
+ });
60
+ } else {
61
+ next.call(data.obj, ...data.args);
62
+ }
63
+ });
64
+ }
65
+
52
66
  if (type !== 'request') {
53
67
  return next();
54
68
  }
@@ -61,6 +75,11 @@ module.exports = function(core) {
61
75
  }
62
76
 
63
77
  store.protect = core.protect.makeSourceContext(req, res);
78
+ if (store.protect.allowed) {
79
+ callNext();
80
+ return;
81
+ }
82
+
64
83
  const {
65
84
  reqData: { headers, uriPath, method },
66
85
  resData,
@@ -97,17 +116,7 @@ module.exports = function(core) {
97
116
  }
98
117
 
99
118
  if (!block) {
100
- setImmediate(() => {
101
- const domain = initDomain(req, res);
102
-
103
- if (domain) {
104
- domain.run(() => {
105
- next.call(data.obj, ...data.args);
106
- });
107
- } else {
108
- next.call(data.obj, ...data.args);
109
- }
110
- });
119
+ callNext();
111
120
  } else {
112
121
  store.protect.block(...block);
113
122
  logger.debug({ block }, 'request blocked by not emitting request event');
@@ -22,6 +22,7 @@ const {
22
22
  isString,
23
23
  traverseKeys,
24
24
  traverseKeysAndValues,
25
+ agentLibIDListTypes
25
26
  } = require('@contrast/common');
26
27
 
27
28
  module.exports = function(core) {
@@ -127,10 +128,30 @@ module.exports = function(core) {
127
128
  }
128
129
  };
129
130
 
130
- inputTracing.nosqlInjectionMongo = function(sourceContext, sinkContext) {
131
+ inputTracing.nosqlInjectionMongo = function (sourceContext, sinkContext) {
131
132
  const ruleId = 'nosql-injection-mongo';
132
- const expansionResults = getResultsByRuleId(ruleId, sourceContext);
133
- const stringInjectionResults = getResultsByRuleId('ssjs-injection', sourceContext);
133
+ const nosqlInjectionMongoResults =
134
+ getResultsByRuleId(ruleId, sourceContext) || [];
135
+ const ssjsInjectionResults =
136
+ getResultsByRuleId('ssjs-injection', sourceContext) || [];
137
+
138
+ const stringInjectionResults = [...ssjsInjectionResults];
139
+ const expansionResults = [];
140
+
141
+ nosqlInjectionMongoResults.forEach((result) => {
142
+ if (
143
+ result.idsList &&
144
+ result.idsList.some(
145
+ (el) =>
146
+ el === agentLibIDListTypes.MONGO_SLEEP ||
147
+ el === agentLibIDListTypes.TRUE_CLAUSE_1
148
+ )
149
+ ) {
150
+ stringInjectionResults.push(result);
151
+ } else {
152
+ expansionResults.push(result);
153
+ }
154
+ });
134
155
 
135
156
  if (expansionResults) {
136
157
  let expansionFindings = null;
@@ -138,7 +159,13 @@ module.exports = function(core) {
138
159
  expansionFindings = handleObjectValue(result, sinkContext.value);
139
160
 
140
161
  if (expansionFindings) {
141
- handleFindings(sourceContext, sinkContext, ruleId, result, expansionFindings);
162
+ handleFindings(
163
+ sourceContext,
164
+ sinkContext,
165
+ ruleId,
166
+ result,
167
+ expansionFindings
168
+ );
142
169
  }
143
170
  }
144
171
  }
@@ -148,30 +175,44 @@ module.exports = function(core) {
148
175
 
149
176
  for (const result of stringInjectionResults) {
150
177
  if (typeof sinkContext.value === 'object') {
151
- traverseKeysAndValues(sinkContext.value, function(path, type, value, obj) {
152
- if (type !== 'Key' && !agentLib.isMongoQueryType(value)) return;
153
-
154
- if (!stringFindings && type == 'Key' && value == '$accumulator') {
155
- stringFindings =
156
- handleStringValue(result, obj[value]?.['init'], agentLib) ||
157
- handleStringValue(result, obj[value]?.['merge'], agentLib) ||
158
- handleStringValue(result, obj[value]?.['finalize'], agentLib) ||
159
- handleStringValue(result, obj[value]?.['accumulate'], agentLib);
160
- }
161
-
162
- if (!stringFindings && type == 'Key' && value == '$function') {
163
- stringFindings =
164
- handleStringValue(result, obj['$function']?.body, agentLib);
165
- }
166
-
167
- if (!stringFindings) {
168
- stringFindings = handleStringValue(result, obj[value], agentLib);
178
+ traverseKeysAndValues(
179
+ sinkContext.value,
180
+ function (path, type, value, obj) {
181
+ if (type !== 'Key' && !agentLib.isMongoQueryType(value)) return;
182
+
183
+ if (!stringFindings && type == 'Key' && value == '$accumulator') {
184
+ stringFindings =
185
+ handleStringValue(result, obj[value]?.init, agentLib) ||
186
+ handleStringValue(result, obj[value]?.merge, agentLib) ||
187
+ handleStringValue(result, obj[value]?.finalize, agentLib) ||
188
+ handleStringValue(result, obj[value]?.accumulate, agentLib);
189
+ }
190
+
191
+ if (!stringFindings && type == 'Key' && value == '$function') {
192
+ stringFindings = handleStringValue(
193
+ result,
194
+ obj['$function']?.body,
195
+ agentLib
196
+ );
197
+ }
198
+
199
+ if (!stringFindings) {
200
+ stringFindings = handleStringValue(
201
+ result,
202
+ obj[value],
203
+ agentLib
204
+ );
205
+ }
206
+
207
+ if (stringFindings) return true;
169
208
  }
170
-
171
- if (stringFindings) return true;
172
- });
209
+ );
173
210
  } else if (typeof sinkContext.value === 'string') {
174
- stringFindings = handleStringValue(result, sinkContext.value, agentLib);
211
+ stringFindings = handleStringValue(
212
+ result,
213
+ sinkContext.value,
214
+ agentLib
215
+ );
175
216
  }
176
217
 
177
218
  if (stringFindings) {
@@ -184,7 +225,13 @@ module.exports = function(core) {
184
225
  sourceContext.resultsMap[ruleId] = [nosqlInjectionResult];
185
226
  }
186
227
 
187
- handleFindings(sourceContext, sinkContext, ruleId, nosqlInjectionResult, stringFindings);
228
+ handleFindings(
229
+ sourceContext,
230
+ sinkContext,
231
+ ruleId,
232
+ nosqlInjectionResult,
233
+ stringFindings
234
+ );
188
235
  }
189
236
  }
190
237
  }
@@ -15,115 +15,101 @@
15
15
 
16
16
  'use strict';
17
17
 
18
- const { isString } = require('@contrast/common');
18
+ const { FS_METHODS, isString } = require('@contrast/common');
19
19
  const { patchType } = require('../constants');
20
20
 
21
- const fsMethods = [
22
- { method: 'access' },
23
- { method: 'accessSync' },
24
- { method: 'appendFile' },
25
- { method: 'appendFileSync' },
26
- { method: 'chmod' },
27
- { method: 'chmodSync' },
28
- { method: 'chown' },
29
- { method: 'chownSync' },
30
- { method: 'copyFile', indices: [0, 1] },
31
- { method: 'copyFileSync', indices: [0, 1] },
32
- { method: 'createReadStream' },
33
- { method: 'createWriteStream' },
34
- { method: 'lchmod' },
35
- { method: 'lchmodSync' },
36
- { method: 'lchown' },
37
- { method: 'lchownSync' },
38
- { method: 'mkdir' },
39
- { method: 'mkdirSync' },
40
- { method: 'open' },
41
- { method: 'openSync' },
42
- { method: 'readFile' },
43
- { method: 'readFileSync' },
44
- { method: 'readdir' },
45
- { method: 'readdirSync' },
46
- { method: 'readlink' },
47
- { method: 'readlinkSync' },
48
- { method: 'rename', indices: [0, 1] },
49
- { method: 'renameSync', indices: [0, 1] },
50
- { method: 'rmdir' },
51
- { method: 'rmdirSync' },
52
- { method: 'symlink', indices: [0, 1] },
53
- { method: 'symlinkSync', indices: [0, 1] },
54
- { method: 'truncate' },
55
- { method: 'truncateSync' },
56
- { method: 'unlink' },
57
- { method: 'unlinkSync' },
58
- { method: 'writeFile' },
59
- { method: 'writeFileSync' },
60
- ];
61
-
62
- module.exports = function(core) {
63
- const {
64
- scopes: { instrumentation },
65
- patcher,
66
- depHooks,
67
- protect,
68
- protect: { inputTracing }
69
- } = core;
70
-
71
- function getValues(indices, args) {
72
- return indices.reduce((acc, idx) => {
73
- const value = args[idx];
74
- if (value && isString(value)) acc.push(value);
75
- return acc;
76
- }, []);
77
- }
78
-
79
- function install() {
80
- depHooks.resolve({ name: 'fs' }, fs => {
81
- fsMethods.forEach(({ method, indices = [0] }) => {
82
- const name = `fs.${method}`;
83
-
84
- // may not exist depending on OS
85
- if (!fs[method]) return;
86
-
87
- patcher.patch(fs, method, {
88
- name,
89
- patchType,
90
- pre({ args, hooked, name, orig }) {
91
- // don't proceed if instrumentation is off e.g. within require() call
92
- if (instrumentation.isLocked()) return;
93
-
94
- // obtain the Protect sourceContext
95
- const sourceContext = protect.getSourceContext('fs');
96
-
97
- if (!sourceContext) return;
98
-
99
- // build list of values on which to perform INPUT TRACING
100
- const values = getValues(indices, args);
101
- if (!values.length) return;
102
-
103
- // while we need to check whether instrumentation is locked above, we
104
- // don't need to necessarily need to lock it here - there are no
105
- // lower-level calls that we instrument
106
- for (const value of values) {
107
- const sinkContext = {
108
- name,
109
- value,
110
- stacktraceOpts: { constructorOpt: hooked, prependFrames: [orig] },
111
- };
112
- inputTracing.handlePathTraversal(sourceContext, sinkContext);
113
- core.protect.semanticAnalysis.handlePathTraversalFileSecurityBypass(sourceContext, sinkContext);
21
+ function getValues(indices, args) {
22
+ return indices.reduce((acc, idx) => {
23
+ const value = args[idx];
24
+ if (value && isString(value)) acc.push(value);
25
+ return acc;
26
+ }, []);
27
+ }
28
+
29
+ module.exports = function init(core) {
30
+ const { depHooks, patcher } = core;
31
+
32
+ const preHook = (indices) => ({ args, hooked, name, orig }) => {
33
+ // don't proceed if instrumentation is off e.g. within require() call
34
+ if (core.scopes.instrumentation.isLocked()) return;
35
+
36
+ // obtain the Protect sourceContext
37
+ const sourceContext = core.protect.getSourceContext('fs');
38
+
39
+ if (!sourceContext) return;
40
+
41
+ // build list of values on which to perform INPUT TRACING
42
+ const values = getValues(indices, args);
43
+ if (!values.length) return;
44
+
45
+ // while we need to check whether instrumentation is locked above, we
46
+ // don't need to necessarily need to lock it here - there are no
47
+ // lower-level calls that we instrument
48
+ for (const value of values) {
49
+ const sinkContext = {
50
+ name,
51
+ value,
52
+ stacktraceOpts: { constructorOpt: hooked, prependFrames: [orig] },
53
+ };
54
+
55
+ core.protect.inputTracing.handlePathTraversal(
56
+ sourceContext,
57
+ sinkContext
58
+ );
59
+ core.protect.semanticAnalysis.handlePathTraversalFileSecurityBypass(
60
+ sourceContext,
61
+ sinkContext,
62
+ );
63
+ }
64
+ };
65
+
66
+ return core.protect.inputTracing.fsInstrumentation = {
67
+ install() {
68
+ depHooks.resolve({ name: 'fs' }, (fs) => {
69
+ for (const method of FS_METHODS) {
70
+ // not all methods are available on every OS or Node version.
71
+ if (fs[method.name]) {
72
+ patcher.patch(fs, method.name, {
73
+ name: `fs.${method.name}`,
74
+ patchType,
75
+ pre: preHook(method.indices)
76
+ });
77
+ }
78
+
79
+ if (method.sync) {
80
+ const syncName = `${method.name}Sync`;
81
+ if (fs[syncName]) {
82
+ patcher.patch(fs, syncName, {
83
+ name: `fs.${syncName}`,
84
+ patchType,
85
+ pre: preHook(method.indices)
86
+ });
114
87
  }
115
88
  }
116
- });
89
+
90
+ if (method.promises && fs.promises && fs.promises[method.name]) {
91
+ patcher.patch(fs.promises, method.name, {
92
+ name: `fs.promises.${method.name}`,
93
+ patchType,
94
+ pre: preHook(method.indices)
95
+ });
96
+ }
97
+ }
117
98
  });
118
- });
119
- }
120
99
 
121
- const fsInstrumentation = inputTracing.fsInstrumentation = {
122
- getValues,
123
- install,
100
+ depHooks.resolve({ name: 'fs/promises' }, (fsPromises) => {
101
+ for (const method of FS_METHODS) {
102
+ if (method.promises && fsPromises[method.name]) {
103
+ patcher.patch(fsPromises, method.name, {
104
+ name: `fsPromises.${method.name}`,
105
+ patchType,
106
+ pre: preHook(method.indices)
107
+ });
108
+ }
109
+ }
110
+ });
111
+ }
124
112
  };
125
-
126
- return fsInstrumentation;
127
113
  };
128
114
 
129
- module.exports.fsMethods = fsMethods;
115
+ module.exports.FS_METHODS = FS_METHODS;
@@ -26,7 +26,7 @@ module.exports = function (core) {
26
26
 
27
27
  function getCursorQueryData(args, version) {
28
28
  const query = semver.gte(version, '3.3.0')
29
- ? args[0]?.cmd?.query
29
+ ? args[0]?.cmd?.query || args[1]?.query
30
30
  : args[1]?.query;
31
31
 
32
32
  if (!query) {
@@ -67,12 +67,12 @@ module.exports = function (core) {
67
67
  inputTracing.nosqlInjectionMongo(sourceContext, sinkContext);
68
68
  }
69
69
 
70
- function v4CollectionVal({ args, ...ctx }) {
70
+ function CollectionVal({ args, ...ctx }) {
71
71
  const value = typeof args[0] == 'function' ? null : args[0];
72
72
  preHook(value, ctx);
73
73
  }
74
74
 
75
- function v4DbVal({ args, ...ctx }) {
75
+ function dbCommandVal({ args, ...ctx }) {
76
76
  const value = args[0]?.filter;
77
77
  preHook(value, ctx);
78
78
  }
@@ -87,7 +87,7 @@ module.exports = function (core) {
87
87
  preHook(value, ctx);
88
88
  }
89
89
 
90
- function v3DbVal({ args, ...ctx }) {
90
+ function firstArgVal({ args, ...ctx }) {
91
91
  const value = args[0];
92
92
  preHook(value, ctx);
93
93
  }
@@ -106,16 +106,19 @@ module.exports = function (core) {
106
106
  [
107
107
  {
108
108
  moduleName,
109
- version: '>=4.0.0',
109
+ version: '>=2.0.0 <3.0.0 || >=4.0.0',
110
110
  patchObjects: [
111
111
  {
112
112
  name: 'Collection.prototype',
113
113
  methods: [
114
+ 'update',
114
115
  'updateOne',
115
116
  'replaceOne',
116
117
  'updateMany',
118
+ 'remove',
117
119
  'deleteOne',
118
120
  'deleteMany',
121
+ 'findAndModify',
119
122
  'findOneAndDelete',
120
123
  'findOneAndReplace',
121
124
  'findOneAndUpdate',
@@ -125,13 +128,13 @@ module.exports = function (core) {
125
128
  'aggregate'
126
129
  ],
127
130
  patchType,
128
- preHookFn: v4CollectionVal
131
+ preHookFn: CollectionVal
129
132
  },
130
133
  {
131
134
  name: 'Db.prototype',
132
135
  methods: ['command'],
133
136
  patchType,
134
- preHookFn: v4DbVal
137
+ preHookFn: dbCommandVal
135
138
  }
136
139
  ]
137
140
  },
@@ -157,18 +160,30 @@ module.exports = function (core) {
157
160
  patchType,
158
161
  preHookFn: v3CursorVal
159
162
  },
163
+ {
164
+ name: 'Db.prototype',
165
+ methods: ['command'],
166
+ patchType,
167
+ preHookFn: dbCommandVal
168
+ },
160
169
  {
161
170
  name: 'Db.prototype',
162
171
  methods: ['eval'],
163
172
  patchType,
164
- preHookFn: v3DbVal
165
- }
173
+ preHookFn: firstArgVal
174
+ },
175
+ {
176
+ name: 'Collection.prototype',
177
+ methods: ['find'],
178
+ patchType,
179
+ preHookFn: firstArgVal
180
+ },
166
181
  ]
167
182
  },
168
183
  {
169
184
  moduleName,
170
185
  file: 'lib/topologies/topology_base.js',
171
- version: '<4.0.0',
186
+ version: '<4.0.0 >=3.0.0',
172
187
  patchObjects: [
173
188
  {
174
189
  name: 'TopologyBase.prototype',
@@ -187,7 +202,7 @@ module.exports = function (core) {
187
202
  {
188
203
  moduleName,
189
204
  file: 'lib/topologies/native_topology.js',
190
- version: '<4.0.0',
205
+ version: '<4.0.0 >=3.0.0',
191
206
  patchObjects: [
192
207
  {
193
208
  name: 'NativeTopology.prototype',
@@ -204,7 +219,7 @@ module.exports = function (core) {
204
219
  preHookFn: v3CursorVal
205
220
  }
206
221
  ]
207
- }
222
+ },
208
223
  ].forEach(({ moduleName, file, version, patchObjects }) => {
209
224
  instrument({
210
225
  moduleName,
@@ -44,7 +44,7 @@ module.exports = function(core) {
44
44
  const policy = getPolicy({ uriPath });
45
45
 
46
46
  // URL exclusions can disable all rules
47
- if (!policy) {
47
+ if (!policy || policy.rulesMask === 0) {
48
48
  return { allowed: true };
49
49
  }
50
50
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/protect",
3
- "version": "1.17.0",
3
+ "version": "1.19.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,11 +17,11 @@
17
17
  "test": "../scripts/test.sh"
18
18
  },
19
19
  "dependencies": {
20
- "@contrast/agent-lib": "^5.3.4",
21
- "@contrast/common": "1.8.0",
22
- "@contrast/core": "1.15.0",
23
- "@contrast/esm-hooks": "1.11.0",
24
- "@contrast/scopes": "1.3.0",
20
+ "@contrast/agent-lib": "^7.0.1",
21
+ "@contrast/common": "1.10.0",
22
+ "@contrast/core": "1.17.0",
23
+ "@contrast/esm-hooks": "1.13.0",
24
+ "@contrast/scopes": "1.4.0",
25
25
  "ipaddr.js": "^2.0.1",
26
26
  "semver": "^7.3.7"
27
27
  },