@contrast/protect 1.66.0 → 1.68.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.
@@ -16,13 +16,12 @@
16
16
  'use strict';
17
17
 
18
18
  const onFinished = require('on-finished');
19
- const { Event, primordials: { StringPrototypeToLowerCase, ArrayPrototypeSlice } } = require('@contrast/common');
19
+ const { primordials: { StringPrototypeToLowerCase, ArrayPrototypeSlice } } = require('@contrast/common');
20
20
  const { patchType } = require('../constants');
21
21
 
22
22
  module.exports = function (core) {
23
23
  const {
24
24
  logger,
25
- messages,
26
25
  scopes: { sources },
27
26
  instrumentation: { instrument },
28
27
  protect: {
@@ -75,8 +74,8 @@ module.exports = function (core) {
75
74
 
76
75
  onFinished(res, (/* err, req */) => {
77
76
  resData.statusCode = res.statusCode;
77
+ // check for probes and method-tampering outcome
78
78
  inputAnalysis.handleRequestEnd(store.protect);
79
- messages.emit(Event.PROTECT, store);
80
79
  });
81
80
 
82
81
  const connectInputs = {
@@ -29,6 +29,7 @@ const {
29
29
 
30
30
  module.exports = function(core) {
31
31
  const {
32
+ protect,
32
33
  protect: {
33
34
  agentLib,
34
35
  inputTracing,
@@ -40,16 +41,21 @@ module.exports = function(core) {
40
41
  function handleFindings(sourceContext, sinkContext, ruleId, result, findings) {
41
42
  const { stacktraceOpts } = sinkContext;
42
43
  captureStacktrace(sinkContext, stacktraceOpts);
43
- result.exploitMetadata.push({ sinkContext, findings });
44
+ result.exploited = true;
44
45
 
45
- const mode = sourceContext.policy[ruleId];
46
+ const mode = sourceContext.policy.getRuleMode(ruleId);
47
+ const eventArg = { findings, result, sinkContext };
46
48
 
49
+ let blockInfo;
47
50
  if (BLOCKING_MODES.includes(mode)) {
48
51
  result.blocked = true;
49
- const blockInfo = [mode, ruleId];
52
+ blockInfo = [mode, ruleId, eventArg];
50
53
  sourceContext.securityException = blockInfo;
51
- throwSecurityException(sourceContext);
52
54
  }
55
+
56
+ protect.reportFinding(eventArg);
57
+
58
+ if (blockInfo) throwSecurityException(sourceContext);
53
59
  }
54
60
 
55
61
  inputTracing.handlePathTraversal = function(sourceContext, sinkContext) {
@@ -61,7 +67,6 @@ module.exports = function(core) {
61
67
  for (const result of results) {
62
68
  const idx = sinkContext.value.indexOf(result.value);
63
69
  const findings = idx !== -1 ? { path: sinkContext.value } : null;
64
-
65
70
  if (findings) {
66
71
  handleFindings(sourceContext, sinkContext, ruleId, result, findings);
67
72
  }
@@ -218,13 +223,7 @@ module.exports = function(core) {
218
223
  }
219
224
 
220
225
  if (stringFindings) {
221
- const nosqlInjectionResult = { ...result, ruleId, mappedId: ruleId };
222
-
223
- // don't modify ssjs-injection result items so use new exploit metadata array here
224
- if (nosqlInjectionResult.idsList?.some?.((id) => id.startsWith('SSJS'))) {
225
- nosqlInjectionResult.exploitMetadata = [];
226
- }
227
-
226
+ const nosqlInjectionResult = { ...result, ruleId, mappedId: ruleId, exploited: false };
228
227
  const nosqlInjectionResults = sourceContext.resultsMap[ruleId];
229
228
  const isAlreadyPresentInNosqlresults = result.idsList &&
230
229
  result.idsList.some(
@@ -312,12 +311,13 @@ module.exports = function(core) {
312
311
  const findings = idx !== -1 ? { value: sinkContext.value } : null;
313
312
 
314
313
  if (findings) {
315
- result.exploitMetadata.push({ sinkContext, findings });
314
+ result.exploited = true;
315
+ handleFindings(sourceContext, sinkContext, ruleId, result, findings);
316
+ break;
316
317
  }
317
318
  }
318
319
  };
319
320
 
320
-
321
321
  return inputTracing;
322
322
  };
323
323
 
@@ -328,7 +328,7 @@ module.exports = function(core) {
328
328
  * @returns {AnalysisResult[]}
329
329
  */
330
330
  function getResultsByRuleId(ruleId, context) {
331
- if (!context.policy || context.policy[ruleId] === OFF) {
331
+ if (!context.policy || context.policy.getRuleMode(ruleId) === OFF) {
332
332
  return;
333
333
  }
334
334
  // because agent-lib stores all nosql-injection results under nosql-injection-mongo
@@ -18,8 +18,6 @@
18
18
  module.exports = function(core) {
19
19
  const { protect } = core;
20
20
 
21
- const DISABLED_POLICY = { allowed: true };
22
-
23
21
  /**
24
22
  * @param {object} param
25
23
  * @param {object} param.store
@@ -33,12 +31,7 @@ module.exports = function(core) {
33
31
  // incomingMessage,
34
32
  serverResponse,
35
33
  }) {
36
- if (!core.config.getEffectiveValue('protect.enable')) return DISABLED_POLICY;
37
-
38
34
  const policy = protect.getPolicy({ uriPath: sourceInfo.uriPath });
39
- // URL exclusions can disable all rules
40
- if (!policy || policy.rulesMask === 0) return DISABLED_POLICY;
41
-
42
35
  const protectStore = {
43
36
  resData: {
44
37
  statusCode: null,
@@ -56,6 +49,11 @@ module.exports = function(core) {
56
49
  resultsMap: Object.create(null),
57
50
  };
58
51
 
52
+ if (policy.allowed) {
53
+ protectStore.allowed = true;
54
+ }
55
+
56
+
59
57
  return protectStore;
60
58
  }
61
59
 
package/lib/policy.js CHANGED
@@ -24,10 +24,10 @@ const {
24
24
  StringPrototypeToLowerCase,
25
25
  StringPrototypeSplit,
26
26
  RegExpPrototypeTest
27
- }
27
+ },
28
+ set,
28
29
  } = require('@contrast/common');
29
30
  const { ConfigSource } = require('@contrast/config');
30
-
31
31
  const { BLOCK_AT_PERIMETER, OFF } = ProtectRuleMode;
32
32
  const {
33
33
  BOT_BLOCKER,
@@ -58,6 +58,121 @@ module.exports = function (core) {
58
58
  protect: { agentLib }
59
59
  } = core;
60
60
 
61
+ // todo: can we not init this and just set what's needed
62
+ let processedExclusions = initCompiled();
63
+
64
+ const policy = protect.policy = {
65
+ version: Date.now(),
66
+ exclusions: processedExclusions
67
+ };
68
+
69
+
70
+ class RequestPolicy {
71
+ constructor(core, sourceInfo) {
72
+ Object.defineProperty(this, 'core', { value: core });
73
+ Object.defineProperty(this, 'sourceInfo', { value: sourceInfo });
74
+
75
+ this.init();
76
+ }
77
+
78
+ init() {
79
+ const { uriPath } = this.sourceInfo;
80
+ this.version = core.protect.policy.version;
81
+
82
+ if (!this.core.config.getEffectiveValue('protect.enable')) {
83
+ this.allowed = true;
84
+ return;
85
+ }
86
+
87
+ // todo build exclusions
88
+ for (const [inputType, exclusions] of Object.entries(processedExclusions)) {
89
+ for (const e of exclusions) {
90
+ if (!e.matchesUriPath(uriPath)) continue;
91
+
92
+ // url exclusions
93
+ if (inputType === 'url') {
94
+ // if applies to all rules, there is no policy for the request i.e. disable protect
95
+ if (!e.policy) {
96
+ this.allowed = true;
97
+ return;
98
+ }
99
+
100
+ // merge exclusion's policy into the request's policy
101
+ for (const key of Object.keys(e.policy)) {
102
+ const value = e.policy[key];
103
+ if (key === 'rulesMask') {
104
+ if (this.exclusions?.rulesMask == null)
105
+ set(this, 'exclusions.rulesMask', this.core.protect.policy.rulesMask);
106
+ // this is how to disable rules bitwise
107
+ this.exclusions.rulesMask = this.exclusions.rulesMask & ~value;
108
+ } else {
109
+ set(this, `exclusions.${key}`, value);
110
+ }
111
+ }
112
+ } else if (inputType === 'querystring') {
113
+ if (!e.policy) {
114
+ set(this, 'exclusions.ignoreQuerystring', true);
115
+ } else {
116
+ // merge exclusion's policy into the querystring's policy
117
+ // this.exclusions.querystringPolicy = this.exclusions.querystringPolicy || {};
118
+ for (const key of Object.keys(e.policy)) {
119
+ const value = e.policy[key];
120
+ if (key !== 'rulesMask') {
121
+ set(this, `exclusions.querystringPolicy.${key}`, value);
122
+ }
123
+ }
124
+ }
125
+ } else if (inputType === 'body') {
126
+ if (!e.policy) {
127
+ set(this, 'exclusions.ignoreBody', true);
128
+ } else {
129
+ // merge exclusion's policy into the querystring's policy
130
+ // set(this, `exclusions.bodyPolicy = this.exclusions.bodyPolicy || {};
131
+ for (const key of Object.keys(e.policy)) {
132
+ const value = e.policy[key];
133
+ if (key !== 'rulesMask') {
134
+ set(this, `exclusions.bodyPolicy.${key}`, value);
135
+ }
136
+ }
137
+ }
138
+ } else {
139
+ // copy matching input exclusions into request policy
140
+ if (!this.exclusions?.[inputType]) set(this, `exclusions.${inputType}`, []);
141
+ this.exclusions[inputType].push(e);
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ checkInit() {
148
+ if (!this.version == core.protect.policy.version) {
149
+ this.init();
150
+ }
151
+ }
152
+
153
+ isDisabled() {
154
+ this.checkInit();
155
+ return this.allowed === true;
156
+ }
157
+
158
+ getRulesMask(inputType) {
159
+ this.checkInit();
160
+ if (this.allowed) return 0;
161
+ return this.exclusions?.rulesMask ?? this.core.protect.policy.rulesMask;
162
+ }
163
+
164
+ getRuleMode(ruleId) {
165
+ this.checkInit();
166
+ if (this.allowed) return OFF;
167
+ return this.exclusions?.[ruleId] ?? this.core.protect.policy[ruleId];
168
+ }
169
+
170
+ getExclusionInfo(key, inputType) {
171
+ this.checkInit();
172
+ return key ? this.exclusions?.[key] : this.exclusions;
173
+ }
174
+ }
175
+
61
176
  function initCompiled() {
62
177
  return {
63
178
  url: [],
@@ -69,12 +184,6 @@ module.exports = function (core) {
69
184
  };
70
185
  }
71
186
 
72
- let compiled = initCompiled();
73
-
74
- const policy = protect.policy = {
75
- exclusions: compiled
76
- };
77
-
78
187
  function regExpCheck(str) {
79
188
  return str.indexOf('*') > 0 ||
80
189
  str.indexOf('.') > 0 ||
@@ -156,96 +265,19 @@ module.exports = function (core) {
156
265
  ruleId = 'nosql-injection-mongo';
157
266
  }
158
267
 
159
- if (protect.agentLib.RuleType[ruleId] && mode !== OFF) {
160
- rulesMask = rulesMask | protect.agentLib.RuleType[ruleId];
268
+ if (agentLib.RuleType[ruleId] && mode !== OFF) {
269
+ rulesMask = rulesMask | agentLib.RuleType[ruleId];
161
270
  }
162
271
  }
163
272
 
164
273
  policy.rulesMask = rulesMask;
165
274
  }
166
275
 
167
- /**
168
- * This gets called by protect.makeSourceContext(). We return copy of policy to avoid
169
- * inconsistent behavior if policy is updated during request handling.
170
- */
171
- function getPolicy({ uriPath } = {}) {
172
- const requestPolicy = {
173
- exclusions: {
174
- ignoreQuerystring: false,
175
- querystringPolicy: null,
176
- ignoreBody: false,
177
- bodyPolicy: null,
178
- header: [],
179
- cookie: [],
180
- parameter: [],
181
- },
182
- rulesMask: policy.rulesMask,
183
- };
184
-
185
- for (const ruleId of Object.values(Rule)) {
186
- requestPolicy[ruleId] = policy[ruleId];
187
- }
188
-
189
- // handle exclusions
190
- for (const [inputType, exclusions] of Object.entries(compiled)) {
191
- for (const e of exclusions) {
192
- if (!e.matchesUriPath(uriPath)) continue;
193
-
194
- // url exclusions
195
- if (inputType === 'url') {
196
- // if applies to all rules, there is no policy for the request i.e. disable protect
197
- if (!e.policy) {
198
- return null;
199
- }
200
-
201
- // merge exclusion's policy into the request's policy
202
- for (const key of Object.keys(e.policy)) {
203
- const value = e.policy[key];
204
- if (key === 'rulesMask') {
205
- // this is how to disable rules bitwise
206
- requestPolicy.rulesMask = requestPolicy.rulesMask & ~value;
207
- } else {
208
- requestPolicy[key] = value;
209
- }
210
- }
211
- } else if (inputType === 'querystring') {
212
- if (!e.policy) {
213
- requestPolicy.exclusions.ignoreQuerystring = true;
214
- } else {
215
- // merge exclusion's policy into the querystring's policy
216
- requestPolicy.exclusions.querystringPolicy = requestPolicy.exclusions.querystringPolicy || {};
217
- for (const key of Object.keys(e.policy)) {
218
- const value = e.policy[key];
219
- if (key !== 'rulesMask') {
220
- requestPolicy.exclusions.querystringPolicy[key] = value;
221
- }
222
- }
223
- }
224
- } else if (inputType === 'body') {
225
- if (!e.policy) {
226
- requestPolicy.exclusions.ignoreBody = true;
227
- } else {
228
- // merge exclusion's policy into the querystring's policy
229
- requestPolicy.exclusions.bodyPolicy = requestPolicy.exclusions.bodyPolicy || {};
230
- for (const key of Object.keys(e.policy)) {
231
- const value = e.policy[key];
232
- if (key !== 'rulesMask') {
233
- requestPolicy.exclusions.bodyPolicy[key] = value;
234
- }
235
- }
236
- }
237
- } else {
238
- // copy matching input exclusions into request policy
239
- requestPolicy.exclusions[inputType].push(e);
240
- }
241
- }
242
- }
243
-
244
- return requestPolicy;
245
- }
246
-
247
276
  function updateGlobalPolicy(remoteSettings) {
248
277
  const protectionRules = remoteSettings?.protect?.rules;
278
+ // last updated
279
+ protect.policy.version = Date.now();
280
+
249
281
  if (protectionRules) {
250
282
  [
251
283
  CMD_INJECTION,
@@ -290,7 +322,8 @@ module.exports = function (core) {
290
322
  }
291
323
 
292
324
  updateRulesMask();
293
- protect.policy.exclusions = compiled;
325
+ protect.policy.exclusions = processedExclusions;
326
+
294
327
  logger.info({ policy: protect.policy }, 'Protect policy updated');
295
328
  }
296
329
  }
@@ -302,7 +335,7 @@ module.exports = function (core) {
302
335
  ].filter((exclusion) => exclusion.modes.includes('defend'));
303
336
 
304
337
  if (!exclusions.length) return;
305
- compiled = initCompiled();
338
+ processedExclusions = initCompiled();
306
339
 
307
340
  for (const exclusionDtm of exclusions) {
308
341
  exclusionDtm.type = exclusionDtm.type || 'URL';
@@ -310,7 +343,7 @@ module.exports = function (core) {
310
343
  const { name, protect_rules, urls, type } = exclusionDtm;
311
344
  const key = StringPrototypeToLowerCase.call(type);
312
345
 
313
- if (!compiled[key]) continue;
346
+ if (!processedExclusions[key]) continue;
314
347
 
315
348
  try {
316
349
  const e = { name };
@@ -354,7 +387,7 @@ module.exports = function (core) {
354
387
  };
355
388
  }
356
389
 
357
- compiled[key].push(e);
390
+ processedExclusions[key].push(e);
358
391
  } catch (err) {
359
392
  logger.error({ err, exclusionDtm }, 'failed to process exclusion');
360
393
  }
@@ -370,5 +403,7 @@ module.exports = function (core) {
370
403
 
371
404
  initPolicy();
372
405
 
373
- return protect.getPolicy = getPolicy;
406
+ return protect.getPolicy = function getPolicy(sourceInfo) {
407
+ return new RequestPolicy(core, sourceInfo);
408
+ };
374
409
  };
@@ -44,6 +44,7 @@ const getRuleResults = function(obj, prop) {
44
44
 
45
45
  module.exports = function(core) {
46
46
  const {
47
+ protect,
47
48
  protect: {
48
49
  agentLib,
49
50
  semanticAnalysis,
@@ -52,27 +53,32 @@ module.exports = function(core) {
52
53
  captureStacktrace,
53
54
  } = core;
54
55
 
55
- function handleResult(sourceContext, sinkContext, ruleId, mode, finding) {
56
+ function handleResult(sourceContext, sinkContext, ruleId, mode, findings) {
56
57
  const { value, stacktraceOpts } = sinkContext;
57
58
  captureStacktrace(sinkContext, stacktraceOpts);
58
59
 
59
60
  // shoehorn findings into agent-lib result data model
60
61
  const result = {
61
62
  blocked: false,
63
+ inputType: InputType.UNKNOWN,
62
64
  ruleId,
63
65
  value,
64
66
  mappedId: ruleId,
65
- exploitMetadata: [{ sinkContext, command: value }],
66
- ...finding
67
+ exploited: true,
67
68
  };
69
+
68
70
  getRuleResults(sourceContext.resultsMap, ruleId).push(result);
69
71
 
72
+ let blockInfo;
70
73
  if (BLOCKING_MODES.includes(mode)) {
71
74
  result.blocked = true;
72
- const blockInfo = [mode, ruleId];
75
+ blockInfo = [mode, ruleId];
73
76
  sourceContext.securityException = blockInfo;
74
- throwSecurityException(sourceContext);
75
77
  }
78
+
79
+ protect.reportFinding({ findings, result, sinkContext });
80
+
81
+ if (blockInfo) throwSecurityException(sourceContext);
76
82
  }
77
83
 
78
84
  /**
@@ -149,7 +155,7 @@ module.exports = function(core) {
149
155
  }
150
156
 
151
157
  semanticAnalysis.handleCmdInjectionSemanticDangerous = function(sourceContext, sinkContext) {
152
- const mode = sourceContext.policy[Rule.CMD_INJECTION_SEMANTIC_DANGEROUS_PATHS];
158
+ const mode = sourceContext.policy.getRuleMode(Rule.CMD_INJECTION_SEMANTIC_DANGEROUS_PATHS);
153
159
 
154
160
  if (mode == OFF) return;
155
161
 
@@ -161,7 +167,7 @@ module.exports = function(core) {
161
167
  };
162
168
 
163
169
  semanticAnalysis.handleCmdInjectionSemanticChainedCommands = function(sourceContext, sinkContext) {
164
- const mode = sourceContext.policy[Rule.CMD_INJECTION_SEMANTIC_CHAINED_COMMANDS];
170
+ const mode = sourceContext.policy.getRuleMode(Rule.CMD_INJECTION_SEMANTIC_CHAINED_COMMANDS);
165
171
 
166
172
  if (mode == OFF) return;
167
173
 
@@ -173,38 +179,33 @@ module.exports = function(core) {
173
179
  };
174
180
 
175
181
  semanticAnalysis.handleCommandInjectionCommandBackdoors = function(sourceContext, sinkContext) {
176
- const mode = sourceContext.policy[Rule.CMD_INJECTION_COMMAND_BACKDOORS];
182
+ const mode = sourceContext.policy.getRuleMode(Rule.CMD_INJECTION_COMMAND_BACKDOORS);
177
183
 
178
184
  if (mode == OFF) return;
179
-
180
185
  const finding = findBackdoorInjection(sourceContext, sinkContext.value);
181
186
 
182
187
  if (finding) {
183
- handleResult(sourceContext, sinkContext, Rule.CMD_INJECTION_COMMAND_BACKDOORS, mode, finding);
188
+ handleResult(sourceContext, sinkContext, Rule.CMD_INJECTION_COMMAND_BACKDOORS, mode);
184
189
  }
185
190
  };
186
191
 
187
192
  semanticAnalysis.handlePathTraversalFileSecurityBypass = function(sourceContext, sinkContext) {
188
- const mode = sourceContext.policy[Rule.PATH_TRAVERSAL_SEMANTIC_FILE_SECURITY_BYPASS];
193
+ const mode = sourceContext.policy.getRuleMode(Rule.PATH_TRAVERSAL_SEMANTIC_FILE_SECURITY_BYPASS);
189
194
 
190
195
  if (mode == OFF) return;
191
196
 
192
197
  if (agentLib.isDangerousPath(sinkContext.value, true)) {
193
- handleResult(sourceContext, sinkContext, Rule.PATH_TRAVERSAL_SEMANTIC_FILE_SECURITY_BYPASS, mode, {
194
- exploitMetadata: [{ sinkContext, path: sinkContext.value }]
195
- });
198
+ handleResult(sourceContext, sinkContext, Rule.PATH_TRAVERSAL_SEMANTIC_FILE_SECURITY_BYPASS, mode);
196
199
  }
197
200
  };
198
201
 
199
202
  semanticAnalysis.handleXXE = function (sourceContext, sinkContext) {
200
- const mode = sourceContext.policy[Rule.XXE];
203
+ const mode = sourceContext.policy.getRuleMode(Rule.XXE);
201
204
  if (mode == OFF) return;
202
205
 
203
206
  const findings = findExternalEntities(sinkContext.value);
204
207
  if (findings.entities.length) {
205
- handleResult(sourceContext, sinkContext, Rule.XXE, mode, {
206
- exploitMetadata: [{ sinkContext, ...findings }],
207
- });
208
+ handleResult(sourceContext, sinkContext, Rule.XXE, mode, findings);
208
209
  }
209
210
  };
210
211
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/protect",
3
- "version": "1.66.0",
3
+ "version": "1.68.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)",
@@ -21,16 +21,16 @@
21
21
  },
22
22
  "dependencies": {
23
23
  "@contrast/agent-lib": "^9.1.0",
24
- "@contrast/common": "1.36.0",
25
- "@contrast/config": "1.51.0",
26
- "@contrast/core": "1.56.0",
27
- "@contrast/dep-hooks": "1.25.0",
28
- "@contrast/esm-hooks": "2.30.0",
29
- "@contrast/instrumentation": "1.35.0",
30
- "@contrast/logger": "1.29.0",
31
- "@contrast/patcher": "1.28.0",
32
- "@contrast/rewriter": "1.32.0",
33
- "@contrast/scopes": "1.26.0",
24
+ "@contrast/common": "1.37.0",
25
+ "@contrast/config": "1.52.1",
26
+ "@contrast/core": "1.57.1",
27
+ "@contrast/dep-hooks": "1.26.1",
28
+ "@contrast/esm-hooks": "2.32.0",
29
+ "@contrast/instrumentation": "1.36.1",
30
+ "@contrast/logger": "1.30.1",
31
+ "@contrast/patcher": "1.29.1",
32
+ "@contrast/rewriter": "1.34.0",
33
+ "@contrast/scopes": "1.27.1",
34
34
  "async-hook-domain": "^4.0.1",
35
35
  "ipaddr.js": "^2.0.1",
36
36
  "on-finished": "^2.4.1",