@contrast/protect 1.20.0 → 1.22.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.
@@ -15,14 +15,14 @@
15
15
 
16
16
  'use strict';
17
17
 
18
- module.exports = function(core) {
18
+ module.exports = function (core) {
19
19
  const { scopes: { sources }, logger } = core;
20
20
 
21
21
  function getSourceContext(callPoint) {
22
22
  const sourceContext = sources.getStore()?.protect;
23
23
 
24
24
  if (!sourceContext) {
25
- logger.debug(`source context not available in ${callPoint}`);
25
+ logger.debug('source context not available in %s', callPoint);
26
26
  return null;
27
27
  }
28
28
 
@@ -15,7 +15,11 @@
15
15
 
16
16
  'use strict';
17
17
 
18
- const { BLOCKING_MODES, isString } = require('@contrast/common');
18
+ const {
19
+ BLOCKING_MODES,
20
+ isString,
21
+ Rule: { UNTRUSTED_DESERIALIZATION }
22
+ } = require('@contrast/common');
19
23
 
20
24
  const NODE_SERIALIZE_RCE_TOKEN = '_$$ND_FUNC$$_';
21
25
 
@@ -37,7 +41,7 @@ module.exports = function(core) {
37
41
  }
38
42
 
39
43
  hardening.handleUntrustedDeserialization = function(sourceContext, sinkContext) {
40
- const ruleId = 'untrusted-deserialization';
44
+ const ruleId = UNTRUSTED_DESERIALIZATION;
41
45
  const mode = sourceContext.policy[ruleId];
42
46
  const { name, value, stacktraceOpts } = sinkContext;
43
47
 
@@ -398,7 +398,7 @@ module.exports = function(core) {
398
398
  };
399
399
 
400
400
  inputAnalysis.handleVirtualPatches = function(sourceContext, requestInput) {
401
- const ruleId = 'virtual-patch';
401
+ const ruleId = Rule.VIRTUAL_PATCH;
402
402
 
403
403
  if (!Object.keys(requestInput).filter(Boolean).length || !sourceContext?.virtualPatchesEvaluators.length) return;
404
404
 
@@ -440,7 +440,7 @@ module.exports = function(core) {
440
440
  };
441
441
 
442
442
  inputAnalysis.handleIpDenylist = function(sourceContext, ipDenylist) {
443
- const ruleId = 'ip-denylist';
443
+ const ruleId = Rule.IP_DENYLIST;
444
444
 
445
445
  if (!sourceContext || !ipDenylist.length) return;
446
446
 
@@ -36,63 +36,80 @@ module.exports = (core) => {
36
36
  * registers a depHook for express module instrumentation
37
37
  */
38
38
  function install() {
39
- depHooks.resolve({ name: 'express', version: '>=4.0.0 <5.0.0', file: 'lib/middleware/query.js' }, (query) => patcher.patch(query, {
40
- name: 'Express.query',
41
- patchType,
42
- post(data) {
43
- data.result = patcher.patch(data.result, {
44
- name: 'Express.query',
45
- patchType,
46
- pre(data) {
47
- const [req, , origNext] = data.args;
48
-
49
- function contrastNext(origErr) {
50
- const sourceContext = protect.getSourceContext('Express.query');
51
- let securityException;
52
-
53
- // It is possible for the query to be already parsed by `qs`
54
- // which means that we've already handled/analyzed it.
55
- // So we check whether we already have the `parsedQuery` property in the context
56
- if (sourceContext && req.query && Object.keys(req.query).length && (!('parsedQuery' in sourceContext))) {
57
- sourceContext.parsedQuery = req.query;
58
-
59
- try {
60
- inputAnalysis.handleQueryParams(sourceContext, req.query);
61
- } catch (err) {
62
- if (isSecurityException(err)) {
63
- securityException = err;
64
- } else {
65
- logger.error({ err }, 'Unexpected error during input analysis');
39
+ depHooks.resolve(
40
+ { name: 'express', version: '>=4.0.0 <5.0.0', file: 'lib/middleware/query.js' },
41
+ (query) => patcher.patch(query, {
42
+ name: 'express.query',
43
+ patchType,
44
+ post(data) {
45
+ data.result = patcher.patch(data.result, {
46
+ name: 'express.query',
47
+ patchType,
48
+ pre(data) {
49
+ const [req, , origNext] = data.args;
50
+
51
+ function contrastNext(origErr) {
52
+ const sourceContext = protect.getSourceContext('express.query');
53
+ let securityException;
54
+
55
+ // It is possible for the query to be already parsed by `qs`
56
+ // which means that we've already handled/analyzed it.
57
+ // So we check whether we already have the `parsedQuery` property in the context
58
+ if (sourceContext && req.query && Object.keys(req.query).length && (!('parsedQuery' in sourceContext))) {
59
+ sourceContext.parsedQuery = req.query;
60
+
61
+ try {
62
+ inputAnalysis.handleQueryParams(sourceContext, req.query);
63
+ } catch (err) {
64
+ if (isSecurityException(err)) {
65
+ securityException = err;
66
+ } else {
67
+ logger.error({ err }, 'Unexpected error during input analysis');
68
+ }
66
69
  }
67
70
  }
71
+
72
+ const error = securityException || origErr;
73
+
74
+ origNext(error);
68
75
  }
69
76
 
70
- const error = securityException || origErr;
77
+ data.args[2] = contrastNext;
78
+ }
79
+ });
80
+ }
81
+ }));
82
+
83
+ depHooks.resolve(
84
+ { name: 'express', version: '>=4.0.0 <5.0.0', file: 'lib/router/layer.js' },
85
+ (Layer) => {
86
+ const name = 'express.Layer.prototype.match';
87
+ patcher.patch(Layer.prototype, 'match', {
88
+ name,
89
+ patchType,
90
+ post(data) {
91
+ const layer = data.obj;
71
92
 
72
- origNext(error);
93
+ // we can exit early if
94
+ // the layer doesn't match the request or
95
+ // the layer doesn't recognize any parameters
96
+ if (!data.result || !layer.keys || layer.keys.length === 0) {
97
+ return;
73
98
  }
74
99
 
75
- data.args[2] = contrastNext;
76
- }
77
- });
78
- }
79
- }));
100
+ const sourceContext = protect.getSourceContext(name);
80
101
 
81
- depHooks.resolve({ name: 'express', version: '>=4.0.0 <5.0.0', file: 'lib/router/layer.js' }, (Layer) => {
82
- patcher.patch(Layer.prototype, 'handle_request', {
83
- name: 'express.Layer.prototype.handle_request',
84
- patchType,
85
- pre(data) {
86
- const { obj: { params } } = data;
87
- const sourceContext = protect.getSourceContext('Express.Layer.handle_request');
102
+ if (!sourceContext) {
103
+ return;
104
+ }
88
105
 
89
- if (sourceContext && params && Object.keys(params).length) {
90
- sourceContext.parsedParams = params;
91
- inputAnalysis.handleUrlParams(sourceContext, params);
106
+ sourceContext.parsedParams = layer.params;
107
+ inputAnalysis.handleUrlParams(sourceContext, layer.params);
92
108
  }
93
- }
109
+ });
110
+
111
+ return Layer;
94
112
  });
95
- });
96
113
  }
97
114
 
98
115
  const express4Instrumentation = inputAnalysis.express4Instrumentation = {
@@ -22,7 +22,8 @@ const {
22
22
  isString,
23
23
  traverseKeys,
24
24
  traverseKeysAndValues,
25
- agentLibIDListTypes
25
+ agentLibIDListTypes,
26
+ Rule: { SQL_INJECTION, PATH_TRAVERSAL, CMD_INJECTION, NOSQL_INJECTION_MONGO, SSJS_INJECTION, REFLECTED_XSS }
26
27
  } = require('@contrast/common');
27
28
 
28
29
  module.exports = function(core) {
@@ -51,7 +52,7 @@ module.exports = function(core) {
51
52
  }
52
53
 
53
54
  inputTracing.handlePathTraversal = function(sourceContext, sinkContext) {
54
- const ruleId = 'path-traversal';
55
+ const ruleId = PATH_TRAVERSAL;
55
56
  const results = getResultsByRuleId(ruleId, sourceContext);
56
57
 
57
58
  if (!results) return;
@@ -67,7 +68,7 @@ module.exports = function(core) {
67
68
  };
68
69
 
69
70
  inputTracing.handleCommandInjection = function(sourceContext, sinkContext) {
70
- const ruleId = 'cmd-injection';
71
+ const ruleId = CMD_INJECTION;
71
72
  const results = getResultsByRuleId(ruleId, sourceContext);
72
73
 
73
74
  if (!results) return;
@@ -93,7 +94,7 @@ module.exports = function(core) {
93
94
  };
94
95
 
95
96
  inputTracing.handleSqlInjection = function(sourceContext, sinkContext) {
96
- const ruleId = 'sql-injection';
97
+ const ruleId = SQL_INJECTION;
97
98
  const results = getResultsByRuleId(ruleId, sourceContext);
98
99
 
99
100
  if (!results) return;
@@ -129,7 +130,7 @@ module.exports = function(core) {
129
130
  };
130
131
 
131
132
  inputTracing.nosqlInjectionMongo = function (sourceContext, sinkContext) {
132
- const ruleId = 'nosql-injection-mongo';
133
+ const ruleId = NOSQL_INJECTION_MONGO;
133
134
  const nosqlInjectionMongoResults =
134
135
  getResultsByRuleId(ruleId, sourceContext) || [];
135
136
  const ssjsInjectionResults =
@@ -238,7 +239,7 @@ module.exports = function(core) {
238
239
  };
239
240
 
240
241
  inputTracing.ssjsInjection = function(sourceContext, sinkContext) {
241
- const ruleId = 'ssjs-injection';
242
+ const ruleId = SSJS_INJECTION;
242
243
  let sinkValuesArr = [];
243
244
 
244
245
  const results = getResultsByRuleId(ruleId, sourceContext);
@@ -289,7 +290,7 @@ module.exports = function(core) {
289
290
  };
290
291
 
291
292
  inputTracing.handleReflectedXss = function(sourceContext, sinkContext) {
292
- const ruleId = 'reflected-xss';
293
+ const ruleId = REFLECTED_XSS;
293
294
  const results = getResultsByRuleId(ruleId, sourceContext);
294
295
 
295
296
  if (!results) return;
@@ -19,64 +19,72 @@ const { isString } = require('@contrast/common');
19
19
  const SecurityException = require('../../security-exception');
20
20
  const { patchType } = require('../constants');
21
21
 
22
- module.exports = function(core) {
22
+ module.exports = function (core) {
23
23
  const {
24
24
  depHooks,
25
25
  patcher,
26
26
  logger,
27
- protect: { semanticAnalysis },
28
27
  protect,
29
28
  } = core;
30
29
 
31
- function install() {
32
- depHooks.resolve({ name: 'libxmljs' }, hookLibXml);
33
- // libxmljs2 is a fork of libxml that has identical signatures.
34
- // the only difference is that libxmljs2 is used by juice-shop and thus
35
- // we're missing sinks in one of our most important sample apps.
36
- depHooks.resolve({ name: 'libxmljs2' }, hookLibXml);
37
- }
30
+ /**
31
+ * @param {boolean} newApi
32
+ */
33
+ const handler = (newApi) => (mod, metadata) => {
34
+ const checkOptions = newApi
35
+ ? (opts) => !opts?.noent && !opts?.replaceEntities
36
+ : (opts) => !opts?.noent;
38
37
 
39
- function hookLibXml(libxmljs) {
40
- patcher.patch(libxmljs, 'parseXmlString', {
41
- name: 'libxmljs.parseXmlString',
42
- patchType,
43
- pre: preParseXmlMethod
44
- });
38
+ const methods = newApi
39
+ ? ['parseXml', 'parseXmlAsync']
40
+ : ['parseXml', 'parseXmlString'];
45
41
 
46
- patcher.patch(libxmljs, 'parseXml', {
47
- name: 'libxmljs.parseXml',
48
- patchType,
49
- pre: preParseXmlMethod
50
- });
51
- }
42
+ methods.forEach((method) => {
43
+ const name = `${metadata.name}:${method}`;
44
+ patcher.patch(mod, method, {
45
+ name,
46
+ patchType,
47
+ pre({ args, hooked, orig }) {
48
+ const sourceContext = protect.getSourceContext(name);
49
+ const [value, options] = args;
52
50
 
53
- function preParseXmlMethod({ args, hooked, orig }) {
54
- const sourceContext = protect.getSourceContext('libxmljs.parseXmlString');
55
- const value = args[0];
51
+ if (!sourceContext || !value || !isString(value) || checkOptions(options)) {
52
+ return;
53
+ }
56
54
 
57
- // If noent isn't set to true than libxml won't load
58
- // external entities, so this isn't vulnerable
59
- // see: https://help.semmle.com/wiki/display/JS/XML+external+entity+expansion
60
- if (!sourceContext || !value || !isString(value) || !args[1].noent) return;
55
+ const sinkContext = {
56
+ name,
57
+ value,
58
+ stacktraceOpts: {
59
+ constructorOpt: hooked,
60
+ prependFrames: [orig]
61
+ },
62
+ };
61
63
 
62
- const sinkContext = {
63
- name: 'libxmljs.parseXmlString',
64
- value,
65
- stacktraceOpts: { constructorOpt: hooked, prependFrames: [orig] },
66
- };
64
+ try {
65
+ protect.semanticAnalysis.handleXXE(sourceContext, sinkContext);
66
+ } catch (err) {
67
+ if (SecurityException.isSecurityException(err)) {
68
+ throw err;
69
+ }
67
70
 
68
- try {
69
- semanticAnalysis.handleXXE(sourceContext, sinkContext);
70
- } catch (err) {
71
- if (SecurityException.isSecurityException(err)) {
72
- throw err;
73
- }
71
+ logger.error({ err }, 'Unexpected error during semantic analysis');
72
+ }
73
+ }
74
+ });
75
+ });
76
+ };
74
77
 
75
- logger.error({ err }, 'Unexpected error during semantic analysis');
76
- }
77
- }
78
+ protect.semanticAnalysis.libxmljs = {
79
+ install() {
80
+ // libxmljs changed its API in version 1.0.0
81
+ depHooks.resolve({ name: 'libxmljs', version: '>=1' }, handler(true));
78
82
 
79
- const libxmljs = semanticAnalysis.libxmljs = { install };
83
+ // libxmljs versions prior to 1.0.0 and libxmljs2 share the same API
84
+ depHooks.resolve({ name: 'libxmljs', version: '<1' }, handler(false));
85
+ depHooks.resolve({ name: 'libxmljs2' }, handler(false));
86
+ }
87
+ };
80
88
 
81
- return libxmljs;
89
+ return protect.semanticAnalysis.libxmljs;
82
90
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/protect",
3
- "version": "1.20.0",
3
+ "version": "1.22.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)",
@@ -18,9 +18,9 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@contrast/agent-lib": "^7.0.1",
21
- "@contrast/common": "1.11.0",
22
- "@contrast/core": "1.18.0",
23
- "@contrast/esm-hooks": "1.14.0",
21
+ "@contrast/common": "1.13.0",
22
+ "@contrast/core": "1.20.0",
23
+ "@contrast/esm-hooks": "1.16.0",
24
24
  "@contrast/scopes": "1.4.0",
25
25
  "ipaddr.js": "^2.0.1",
26
26
  "semver": "^7.3.7"