@contrast/protect 1.3.0 → 1.5.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.
Files changed (49) hide show
  1. package/lib/error-handlers/index.js +7 -4
  2. package/lib/error-handlers/install/express4.js +2 -3
  3. package/lib/error-handlers/install/{fastify3.js → fastify.js} +13 -15
  4. package/lib/error-handlers/install/hapi.js +75 -0
  5. package/lib/error-handlers/install/koa2.js +1 -2
  6. package/lib/{cli-rewriter.js → get-source-context.js} +13 -15
  7. package/lib/hardening/constants.js +20 -0
  8. package/lib/hardening/handlers.js +65 -0
  9. package/lib/hardening/index.js +29 -0
  10. package/lib/hardening/install/node-serialize0.js +59 -0
  11. package/lib/index.d.ts +3 -21
  12. package/lib/index.js +6 -46
  13. package/lib/input-analysis/handlers.js +198 -39
  14. package/lib/input-analysis/index.js +9 -6
  15. package/lib/input-analysis/install/body-parser1.js +20 -18
  16. package/lib/input-analysis/install/cookie-parser1.js +13 -15
  17. package/lib/input-analysis/install/express4.js +8 -13
  18. package/lib/input-analysis/install/fastify.js +86 -0
  19. package/lib/input-analysis/install/formidable1.js +4 -5
  20. package/lib/input-analysis/install/hapi.js +106 -0
  21. package/lib/input-analysis/install/http.js +80 -30
  22. package/lib/input-analysis/install/koa-body5.js +5 -10
  23. package/lib/input-analysis/install/koa-bodyparser4.js +6 -10
  24. package/lib/input-analysis/install/koa2.js +13 -24
  25. package/lib/input-analysis/install/multer1.js +5 -6
  26. package/lib/input-analysis/install/qs6.js +7 -11
  27. package/lib/input-analysis/install/universal-cookie4.js +3 -7
  28. package/lib/input-analysis/ip-analysis.js +76 -0
  29. package/lib/input-analysis/virtual-patches.js +109 -0
  30. package/lib/input-tracing/handlers/index.js +92 -23
  31. package/lib/input-tracing/index.js +14 -18
  32. package/lib/input-tracing/install/child-process.js +13 -7
  33. package/lib/input-tracing/install/eval.js +60 -0
  34. package/lib/input-tracing/install/fs.js +4 -2
  35. package/lib/input-tracing/install/function.js +60 -0
  36. package/lib/input-tracing/install/http.js +63 -0
  37. package/lib/input-tracing/install/mongodb.js +20 -20
  38. package/lib/input-tracing/install/mysql.js +3 -2
  39. package/lib/input-tracing/install/postgres.js +5 -4
  40. package/lib/input-tracing/install/sequelize.js +7 -5
  41. package/lib/input-tracing/install/sqlite3.js +6 -4
  42. package/lib/input-tracing/install/vm.js +132 -0
  43. package/lib/make-source-context.js +8 -49
  44. package/lib/policy.js +134 -0
  45. package/lib/semantic-analysis/handlers.js +161 -0
  46. package/lib/semantic-analysis/index.js +38 -0
  47. package/package.json +7 -9
  48. package/lib/input-analysis/install/fastify3.js +0 -107
  49. package/lib/utils.js +0 -84
@@ -22,7 +22,7 @@ module.exports = (core) => {
22
22
  depHooks,
23
23
  patcher,
24
24
  logger,
25
- scopes: { sources },
25
+ protect,
26
26
  protect: { inputAnalysis },
27
27
  } = core;
28
28
 
@@ -36,12 +36,11 @@ module.exports = (core) => {
36
36
  const origCb = data.args[1];
37
37
 
38
38
  function hookedCb(...cbArgs) {
39
- const sourceContext = sources.getStore()?.protect;
39
+ const sourceContext = protect.getSourceContext('formidable');
40
+
40
41
  const [, fields, files] = cbArgs;
41
42
 
42
- if (!sourceContext) {
43
- logger.debug('source context not available in `formidable` hook');
44
- } else {
43
+ if (sourceContext) {
45
44
  if (fields) {
46
45
  sourceContext.parsedBody = fields;
47
46
  inputAnalysis.handleParsedBody(sourceContext, fields);
@@ -0,0 +1,106 @@
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 { patchType } = require('../constants');
19
+ const { isSecurityException } = require('../../security-exception');
20
+
21
+ module.exports = (core) => {
22
+ const {
23
+ depHooks,
24
+ patcher,
25
+ logger,
26
+ protect,
27
+ protect: { inputAnalysis },
28
+ } = core;
29
+
30
+ /**
31
+ * registers a depHook for hapi module instrumentation
32
+ */
33
+ function install() {
34
+ depHooks.resolve(
35
+ { name: 'hapi', version: '>=18 <21' },
36
+ registerServerHandler
37
+ );
38
+ depHooks.resolve(
39
+ { name: '@hapi/hapi', version: '>=18 <21' },
40
+ registerServerHandler
41
+ );
42
+ }
43
+
44
+ const registerServerHandler = (hapi) => {
45
+ patcher.patch(hapi, 'server', {
46
+ name: 'hapi.server',
47
+ patchType,
48
+ post(data) {
49
+ const server = data.result;
50
+ if (server) {
51
+ instrumentServer(server);
52
+ } else {
53
+ logger.error('Hapi Server is called but there is no server instance!');
54
+ }
55
+ }
56
+ });
57
+ };
58
+
59
+ const instrumentServer = (server) => {
60
+ server.ext('onPreStart', function onPreStart(core) {
61
+ logger.info('hapi version %s', core.version);
62
+ });
63
+
64
+ server.ext('onPreHandler', function onPreHandler(req, h) {
65
+ const sourceContext = protect.getSourceContext('hapi.onPreHandler');
66
+
67
+ if (sourceContext) {
68
+ try {
69
+ if (req.params && Object.keys(req.params).length) {
70
+ sourceContext.parsedParams = req.params;
71
+ inputAnalysis.handleUrlParams(sourceContext, req.params);
72
+ }
73
+
74
+ if (req.cookies && Object.keys(req.cookies).length) {
75
+ sourceContext.parsedCookies = req.cookies;
76
+ inputAnalysis.handleCookies(sourceContext, req.cookies);
77
+ }
78
+
79
+ if (req.payload && Object.keys(req.payload).length) {
80
+ sourceContext.parsedBody = req.payload;
81
+ inputAnalysis.handleParsedBody(sourceContext, req.payload);
82
+ }
83
+
84
+ if (req.query && Object.keys(req.query).length) {
85
+ sourceContext.parsedQuery = req.query;
86
+ inputAnalysis.handleQueryParams(sourceContext, req.query);
87
+ }
88
+ } catch (err) {
89
+ if (isSecurityException(err)) {
90
+ throw err;
91
+ } else {
92
+ logger.error({ err }, 'Unexpected error during input analysis');
93
+ }
94
+ }
95
+ }
96
+
97
+ return h.continue;
98
+ });
99
+ };
100
+
101
+ const hapiInstrumentation = inputAnalysis.hapiInstrumentation = {
102
+ install
103
+ };
104
+
105
+ return hapiInstrumentation;
106
+ };
@@ -33,8 +33,8 @@ class HttpInstrumentation {
33
33
  this.config = core.config;
34
34
  this.logger = logger.child({ name: 'contrast:protect:input-analysis' });
35
35
  this.depHooks = core.depHooks;
36
- this.messages = core.messages;
37
36
  this.protect = core.protect;
37
+ this.patcher = core.patcher;
38
38
  this.makeSourceContext = this.protect.makeSourceContext;
39
39
  this.maxBodySize = 16 * 1024 * 1024;
40
40
  this.installed = false;
@@ -52,6 +52,7 @@ class HttpInstrumentation {
52
52
  this.installed = true;
53
53
  this.hookHttp();
54
54
  this.hookHttps();
55
+ this.hookHttp2();
55
56
  }
56
57
 
57
58
  uninstall() {
@@ -63,7 +64,7 @@ class HttpInstrumentation {
63
64
  */
64
65
  hookHttp() {
65
66
  this.logger.debug('hooking library: http');
66
- this.depHooks.resolve({ name: 'http' }, this.hookServer.bind(this));
67
+ this.depHooks.resolve({ name: 'http' }, (http) => this.hookServerEmit.call(this, http, 'httpServer'));
67
68
  }
68
69
 
69
70
  /**
@@ -71,36 +72,78 @@ class HttpInstrumentation {
71
72
  */
72
73
  hookHttps() {
73
74
  this.logger.debug('hooking library: https');
74
- this.depHooks.resolve({ name: 'https' }, this.hookServer.bind(this));
75
+ this.depHooks.resolve({ name: 'https' }, (https) => this.hookServerEmit.call(this, https, 'httpsServer'));
76
+ }
77
+
78
+ /**
79
+ * Sets hooks to instrument `http2 Servers`.
80
+ */
81
+ hookHttp2() {
82
+ this.logger.debug('hooking library: http2');
83
+ // http2 library does not expose its Server class, so we need to hook the createServer function
84
+ this.depHooks.resolve({ name: 'http2' }, (http2) => this.hookCreateServer.call(this, http2, 'http2Server'));
85
+ this.depHooks.resolve({ name: 'http2' }, (http2) => this.hookCreateServer.call(this, http2, 'http2SecureServer', 'createSecureServer'));
86
+
87
+ this.logger.debug('hooking library: spdy');
88
+ this.depHooks.resolve({ name: 'spdy' }, (spdy) => this.hookServerEmit.call(this, spdy, 'spdyServer'));
75
89
  }
76
90
 
77
91
  /**
78
- * Instruments the `Server` prototype from `http(s)`. This patches `emit` and
92
+ * Instruments the `Server` prototype from `http(s)` or spdy's http2 Server. This patches `emit` and
79
93
  * invokes the protect service to do analysis when appropriate.
80
- *
81
- * @param {Object} xport The http(s) module export
82
94
  */
83
- hookServer(xport) {
95
+ hookServerEmit(serverSource, sourceName) {
96
+ serverSource.Server.prototype = this.patcher.patch(serverSource.Server.prototype, 'emit', {
97
+ name: `${sourceName}.Server.prototype.emit`,
98
+ patchType: 'initiate-handling',
99
+ around: this.emitAroundHook.bind(this)
100
+ });
101
+ }
102
+
103
+ /**
104
+ * Instruments the `Http2Server` prototype which results from the http2.createServer/createSecureServer() call.
105
+ * This also patches `emit` and
106
+ * invokes the protect service to do analysis when appropriate.
107
+ */
108
+ hookCreateServer(serverSource, sourceName, constructorName = 'createServer') {
84
109
  const self = this;
85
110
 
86
- const {
87
- Server: {
88
- prototype: { emit }
89
- }
90
- } = xport;
111
+ return this.patcher.patch(serverSource, constructorName, {
112
+ name: sourceName,
113
+ patchType: 'initiate-handling',
114
+ post(data) {
115
+
116
+ const { result: server } = data;
117
+ const serverPrototype = server ? Object.getPrototypeOf(server) : null;
91
118
 
92
- xport.Server.prototype.emit = function(...args) {
93
- const [type] = args;
119
+ if (!serverPrototype) {
120
+ self.logger.error('Unable to patch server prototype, continue without instrumentation');
121
+ return;
122
+ }
94
123
 
95
- if (type !== 'request') {
96
- return emit.call(this, ...args);
124
+ self.patcher.patch(serverPrototype, 'emit', {
125
+ name: `${sourceName}.Server.prototype.emit`,
126
+ patchType: 'req-async-storage',
127
+ around: self.emitAroundHook.bind(self)
128
+ });
97
129
  }
130
+ });
131
+ }
98
132
 
99
- const context = { instance: this, method: emit, args };
100
- self.initiateRequestHandling(context);
133
+ /**
134
+ * The around hook for `emit` that
135
+ * invokes the protect service to do analysis when appropriate.
136
+ */
137
+ emitAroundHook(next, data) {
138
+ const [type] = data.args;
139
+
140
+ if (type !== 'request') {
141
+ return next();
142
+ }
101
143
 
102
- return !!this._events[type];
103
- };
144
+ const context = { instance: data.obj, method: next, args: data.args };
145
+ this.initiateRequestHandling(context);
146
+ return !!data.obj._events[type];
104
147
  }
105
148
 
106
149
  /**
@@ -118,15 +161,6 @@ class HttpInstrumentation {
118
161
  args: [, req, res]
119
162
  } = fnContext;
120
163
 
121
- // URL exclusions should be applied here. there is no point in doing any additional
122
- // work if the url is excluded for a particular rule, i.e., that rule should be removed
123
- // from the list of rules for this request. and if all rules are excluded for this url
124
- // then none of the following needs to be done.
125
- if (this.protect.rules.agentLibRulesMask === 0) {
126
- this.logger.debug('no agent-lib rules are enabled, not checking request');
127
- return;
128
- }
129
-
130
164
  let store;
131
165
  let block;
132
166
 
@@ -137,8 +171,10 @@ class HttpInstrumentation {
137
171
  // so that an async context is present.
138
172
  store = this.scope.getStore();
139
173
  // nothing can be done if async context is not available.
174
+
140
175
  if (!store) {
141
176
  this.logger.debug('cannot acquire store for initiateRequestHandling()');
177
+ setImmediate(() => method.call(instance, ...args));
142
178
  return;
143
179
  }
144
180
 
@@ -166,17 +202,31 @@ class HttpInstrumentation {
166
202
  const connectInputs = {
167
203
  headers: HttpInstrumentation.removeCookies(reqData.headers),
168
204
  uriPath: reqData.uriPath,
205
+ rawUrl: req.url,
169
206
  // TODO AGENT-203 - need to handle method-tampering rule.
170
207
  method: reqData.method,
171
208
  };
209
+
172
210
  // only add queries if it's known that 'qs' or equivalent won't be used.
173
211
  /* c8 ignore next 3 */
174
212
  if (reqData.standardUrlParsing) {
175
213
  connectInputs.queries = reqData.queries;
176
214
  }
177
215
 
178
- block = inputAnalysis.handleConnect(store.protect, connectInputs);
216
+ if (inputAnalysis.virtualPatchesEvaluators?.length) {
217
+ store.protect.virtualPatchesEvaluators.push(...inputAnalysis.virtualPatchesEvaluators.map((e) => new Map(e)));
218
+ }
219
+
220
+ if (inputAnalysis.ipDenylist?.length) {
221
+ block = inputAnalysis.handleIpDenylist(store.protect, inputAnalysis.ipDenylist);
222
+ }
223
+
224
+ if (inputAnalysis.ipAllowlist?.length) {
225
+ const allowed = inputAnalysis.handleIpAllowlist(store.protect, inputAnalysis.ipAllowlist);
226
+ if (!block) Object.assign(store.protect, { allowed });
227
+ }
179
228
 
229
+ block = block || inputAnalysis.handleConnect(store.protect, connectInputs);
180
230
  } catch (err) {
181
231
  this.logger.error({ err }, 'Error during input analysis');
182
232
  }
@@ -21,8 +21,7 @@ module.exports = (core) => {
21
21
  const {
22
22
  depHooks,
23
23
  patcher,
24
- logger,
25
- scopes: { sources },
24
+ protect,
26
25
  protect: { inputAnalysis },
27
26
  } = core;
28
27
 
@@ -39,15 +38,11 @@ module.exports = (core) => {
39
38
  const [ctx, origNext] = data.args;
40
39
 
41
40
  async function contrastNext(origErr) {
42
- const sourceContext = sources.getStore()?.protect;
41
+ const sourceContext = protect.getSourceContext('koa-body');
43
42
 
44
- if (!sourceContext) {
45
- logger.debug('source context not available in `koa-body` hook');
46
- } else {
47
- if (ctx.request.body && Object.keys(ctx.request.body).length) {
48
- sourceContext.parsedBody = ctx.request.body;
49
- inputAnalysis.handleParsedBody(sourceContext, ctx.request.body);
50
- }
43
+ if (sourceContext && ctx.request.body && Object.keys(ctx.request.body).length) {
44
+ sourceContext.parsedBody = ctx.request.body;
45
+ inputAnalysis.handleParsedBody(sourceContext, ctx.request.body);
51
46
  }
52
47
 
53
48
  await origNext(origErr);
@@ -21,8 +21,7 @@ module.exports = (core) => {
21
21
  const {
22
22
  depHooks,
23
23
  patcher,
24
- logger,
25
- scopes: { sources },
24
+ protect,
26
25
  protect: { inputAnalysis },
27
26
  } = core;
28
27
 
@@ -39,15 +38,12 @@ module.exports = (core) => {
39
38
  const [ctx, origNext] = data.args;
40
39
 
41
40
  async function contrastNext(origErr) {
42
- const sourceContext = sources.getStore()?.protect;
41
+ const sourceContext = protect.getSourceContext('koa-bodyparser');
43
42
 
44
- if (!sourceContext) {
45
- logger.debug('source context not available in `koa-bodyparser` hook');
46
- } else {
47
- if (ctx.request.body && Object.keys(ctx.request.body).length) {
48
- sourceContext.parsedBody = ctx.request.body;
49
- inputAnalysis.handleParsedBody(sourceContext, ctx.request.body);
50
- }
43
+
44
+ if (sourceContext && ctx.request.body && Object.keys(ctx.request.body).length) {
45
+ sourceContext.parsedBody = ctx.request.body;
46
+ inputAnalysis.handleParsedBody(sourceContext, ctx.request.body);
51
47
  }
52
48
 
53
49
  await origNext(origErr);
@@ -26,8 +26,7 @@ module.exports = (core) => {
26
26
  const {
27
27
  depHooks,
28
28
  patcher,
29
- logger,
30
- scopes: { sources },
29
+ protect,
31
30
  protect: { inputAnalysis },
32
31
  } = core;
33
32
 
@@ -38,11 +37,9 @@ module.exports = (core) => {
38
37
  depHooks.resolve({ name: 'koa', version: '>=2.3.0' }, (Koa) => {
39
38
  function contrastStartMiddleware(ctx, next) {
40
39
  if (ctx.query && Object.keys(ctx.query).length) {
41
- const sourceContext = sources.getStore()?.protect;
40
+ const sourceContext = protect.getSourceContext('Koa startMiddleware');
42
41
 
43
- if (!sourceContext) {
44
- logger.debug('source context not available in `qs` hook');
45
- } else if (!('parsedQuery' in sourceContext)) {
42
+ if (sourceContext && !('parsedQuery' in sourceContext)) {
46
43
  sourceContext.parsedQuery = ctx.query;
47
44
  inputAnalysis.handleQueryParams(sourceContext, ctx.query);
48
45
  }
@@ -76,15 +73,11 @@ module.exports = (core) => {
76
73
  name: `[${router}].layer.prototype`,
77
74
  patchType,
78
75
  post({ result }) {
79
- const sourceContext = sources.getStore()?.protect;
80
-
81
- if (!sourceContext) {
82
- logger.debug(`source context not available in \`[${router}].layer\` hook`);
83
- } else {
84
- if (Object.keys(result).length) {
85
- sourceContext.parsedParams = result;
86
- inputAnalysis.handleUrlParams(sourceContext, result);
87
- }
76
+ const sourceContext = protect.getSourceContext(`[${router}].layer`);
77
+
78
+ if (sourceContext && Object.keys(result).length) {
79
+ sourceContext.parsedParams = result;
80
+ inputAnalysis.handleUrlParams(sourceContext, result);
88
81
  }
89
82
  }
90
83
  });
@@ -107,15 +100,11 @@ module.exports = (core) => {
107
100
  const [ctx, origNext] = data.args;
108
101
 
109
102
  async function contrastNext(origErr) {
110
- const sourceContext = sources.getStore()?.protect;
111
-
112
- if (!sourceContext) {
113
- logger.debug('source context not available in `koa-cookie` hook');
114
- } else {
115
- if (ctx.cookie) {
116
- sourceContext.parsedCookies = ctx.cookie;
117
- inputAnalysis.handleCookies(sourceContext, ctx.cookie);
118
- }
103
+ const sourceContext = protect.getSourceContext('koa-cookie');
104
+
105
+ if (sourceContext && ctx.cookie) {
106
+ sourceContext.parsedCookies = ctx.cookie;
107
+ inputAnalysis.handleCookies(sourceContext, ctx.cookie);
119
108
  }
120
109
 
121
110
  await origNext(origErr);
@@ -23,7 +23,8 @@ module.exports = (core) => {
23
23
  depHooks,
24
24
  patcher,
25
25
  logger,
26
- scopes: { sources, wrap },
26
+ scopes: { wrap },
27
+ protect,
27
28
  protect: { inputAnalysis },
28
29
  } = core;
29
30
 
@@ -41,11 +42,9 @@ module.exports = (core) => {
41
42
 
42
43
  // We are getting the sourceContext here because in the time of calling
43
44
  // the contrastNext() method the context is lost
44
- const sourceContext = sources.getStore()?.protect;
45
- if (!sourceContext) {
46
- logger.debug('source context not available in `multer` hook');
47
- return;
48
- }
45
+ const sourceContext = protect.getSourceContext('multer');
46
+
47
+ if (!sourceContext) return;
49
48
 
50
49
  function contrastNext(origErr) {
51
50
  let securityException;
@@ -21,8 +21,7 @@ module.exports = (core) => {
21
21
  const {
22
22
  depHooks,
23
23
  patcher,
24
- logger,
25
- scopes: { sources },
24
+ protect,
26
25
  protect: { inputAnalysis },
27
26
  } = core;
28
27
 
@@ -34,16 +33,13 @@ module.exports = (core) => {
34
33
  patchType,
35
34
  post({ args, result }) {
36
35
  if (result && Object.keys(result).length) {
37
- const sourceContext = sources.getStore()?.protect;
36
+ const sourceContext = protect.getSourceContext('qs');
38
37
 
39
- if (!sourceContext) {
40
- logger.debug('source context not available in `qs` hook');
41
-
42
- // We need to run analysis for the `qs` result only when it's used as a query parser.
43
- // `qs` is used also for parsing bodies, but these cases we handle individually with
44
- // the respective library that's using it (e.g. `formidable`, `co-body`) because in
45
- // some cases its use is optional and we cannot rely on it.
46
- } else if (sourceContext.reqData?.queries === args[0]) {
38
+ // We need to run analysis for the `qs` result only when it's used as a query parser.
39
+ // `qs` is used also for parsing bodies, but these cases we handle individually with
40
+ // the respective library that's using it (e.g. `formidable`, `co-body`) because in
41
+ // some cases its use is optional and we cannot rely on it.
42
+ if (sourceContext && sourceContext.reqData?.queries === args[0]) {
47
43
  sourceContext.parsedQuery = result;
48
44
  inputAnalysis.handleQueryParams(sourceContext, result);
49
45
  }
@@ -21,12 +21,10 @@ module.exports = (core) => {
21
21
  const {
22
22
  depHooks,
23
23
  patcher,
24
- logger,
25
- scopes: { sources },
24
+ protect,
26
25
  protect: { inputAnalysis },
27
26
  } = core;
28
27
 
29
-
30
28
  // Patch `universal-cookie` package
31
29
  function install() {
32
30
  depHooks.resolve({ name: 'universal-cookie', file: 'cjs/utils.js' }, (uCookieUtils) => patcher.patch(uCookieUtils, 'parseCookies', {
@@ -34,11 +32,9 @@ module.exports = (core) => {
34
32
  patchType,
35
33
  post({ result }) {
36
34
  if (result && Object.keys(result).length) {
37
- const sourceContext = sources.getStore()?.protect;
35
+ const sourceContext = protect.getSourceContext('universal-cookie');
38
36
 
39
- if (!sourceContext) {
40
- logger.debug('source context not available in `universal-cookie` hook');
41
- } else {
37
+ if (sourceContext) {
42
38
  sourceContext.parsedCookies = result;
43
39
  inputAnalysis.handleCookies(sourceContext, result);
44
40
  }
@@ -0,0 +1,76 @@
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 { Event } = require('@contrast/common');
19
+ const address = require('ipaddr.js');
20
+
21
+ module.exports = (core) => {
22
+ const {
23
+ messages,
24
+ protect: { inputAnalysis },
25
+ } = core;
26
+
27
+ const ipAllowlist = inputAnalysis.ipAllowlist = [];
28
+ const ipDenylist = inputAnalysis.ipDenylist = [];
29
+
30
+ messages.on(Event.SERVER_SETTINGS_UPDATE, (serverUpdate) => {
31
+ const now = new Date().getTime();
32
+ const updatedIpAllowList = serverUpdate.features?.defend?.ipAllowlist.map((ipEntry) => ipEntryMap(ipEntry, now));
33
+ const updatedIpDenyList = serverUpdate.features?.defend?.ipDenylist.map((ipEntry) => ipEntryMap(ipEntry, now));
34
+
35
+ if (updatedIpAllowList) {
36
+ ipAllowlist.length = 0;
37
+ ipAllowlist.push(...updatedIpAllowList);
38
+ }
39
+ if (updatedIpDenyList) {
40
+ ipDenylist.length = 0;
41
+ ipDenylist.push(...updatedIpDenyList);
42
+ }
43
+ });
44
+ };
45
+
46
+ function ipEntryMap(ipEntry, startTime) {
47
+ const { ip, expires } = ipEntry;
48
+ let doesExpire, expiresAt, cidr;
49
+
50
+ if (expires) {
51
+ doesExpire = true;
52
+ expiresAt = startTime + expires;
53
+ } else {
54
+ doesExpire = false;
55
+ }
56
+
57
+ const slashIdx = ip.indexOf('/');
58
+ const isCIDR = slashIdx >= 0;
59
+ const ipInstance = isCIDR
60
+ ? address.process(ip.substr(0, slashIdx))
61
+ : address.process(ip);
62
+
63
+ const normalizedValue = ipInstance.toNormalizedString();
64
+
65
+ if (isCIDR) {
66
+ cidr = { range: address.parseCIDR(ip), kind: ipInstance.kind() };
67
+ }
68
+
69
+ return {
70
+ ...ipEntry,
71
+ doesExpire,
72
+ expiresAt,
73
+ normalizedValue,
74
+ cidr
75
+ };
76
+ }