@contrast/protect 1.9.1 → 1.11.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.
@@ -29,6 +29,26 @@ module.exports = function(core) {
29
29
 
30
30
  const express4ErrorHandler = protect.errorHandlers.express4ErrorHandler = {};
31
31
 
32
+ function aroundFn(name) {
33
+ return function around(orig, data) {
34
+ const [err] = data.args;
35
+ const sourceContext = protect.getSourceContext(name);
36
+ const isSecurityException = SecurityException.isSecurityException(err);
37
+
38
+ if (isSecurityException && sourceContext) {
39
+ const blockInfo = sourceContext.securityException;
40
+
41
+ sourceContext.block(...blockInfo);
42
+ return;
43
+ }
44
+
45
+ if (!sourceContext && isSecurityException) {
46
+ logger.info('source context not found; unable to handle response');
47
+ }
48
+ return orig();
49
+ };
50
+ }
51
+
32
52
  express4ErrorHandler.install = function () {
33
53
  depHooks.resolve({ name: 'finalhandler' }, (finalhandler) =>
34
54
  patcher.patch(finalhandler, {
@@ -38,23 +58,7 @@ module.exports = function(core) {
38
58
  data.result = patcher.patch(data.result, {
39
59
  name: 'finalHandler.returnedFunction',
40
60
  patchType,
41
- around(orig, data) {
42
- const [err] = data.args;
43
- const sourceContext = protect.getSourceContext('finalHandler');
44
- const isSecurityException = SecurityException.isSecurityException(err);
45
-
46
- if (isSecurityException && sourceContext) {
47
- const blockInfo = sourceContext.securityException;
48
-
49
- sourceContext.block(...blockInfo);
50
- return;
51
- }
52
-
53
- if (!sourceContext && isSecurityException) {
54
- logger.info('source context not found; unable to handle response');
55
- }
56
- return orig();
57
- },
61
+ around: aroundFn('finalHandler')
58
62
  });
59
63
  },
60
64
  })
@@ -64,23 +68,7 @@ module.exports = function(core) {
64
68
  patcher.patch(Layer.prototype, 'handle_error', {
65
69
  name: 'Layer.prototype.handle_error',
66
70
  patchType,
67
- around(orig, data) {
68
- const [err] = data.args;
69
- const sourceContext = protect.getSourceContext('express.Layer.handle_error');
70
- const isSecurityException = SecurityException.isSecurityException(err);
71
-
72
- if (isSecurityException && sourceContext) {
73
- const blockInfo = sourceContext.securityException;
74
-
75
- sourceContext.block(...blockInfo);
76
- return;
77
- }
78
-
79
- if (!sourceContext && isSecurityException) {
80
- logger.info('source context not found; unable to handle response');
81
- }
82
- return orig();
83
- }
71
+ around: aroundFn('express.Layer.handle_error')
84
72
  });
85
73
 
86
74
  // This should be revisited after the research ticket NODE-2556
package/lib/index.d.ts CHANGED
@@ -26,28 +26,6 @@ type Http = typeof http;
26
26
  type Https = typeof https;
27
27
 
28
28
  export type Block = (mode: string, ruleId: string) => void;
29
- export class HttpInstrumentation {
30
- messages: Messages;
31
- scope: Sources;
32
- config: Config;
33
- logger: Logger;
34
- depHooks: RequireHook;
35
- protect: ProtectMessage;
36
- makeSourceContext: Protect['makeSourceContext'];
37
- maxBodySize: number;
38
- installed: boolean;
39
-
40
- constructor(core: any);
41
-
42
- install(): void;
43
- uninstall(): void; //NYI
44
- hookHttp(): void;
45
- hookHttps(): void;
46
- hookServer(xport: Http | Https): void;
47
- initiateRequestHandling(fnContext: { instance: any, method: any, args: any }): void; //TODO
48
- removeCookies(headers: string[]): string[];
49
- }
50
-
51
29
  export interface ProtectRequestStore {
52
30
  reqData: ReqData;
53
31
  block: Block;
@@ -95,7 +73,7 @@ export interface Protect {
95
73
  expressInstrumentation: { install: () => void },
96
74
  fastifyInstrumentation: { install: () => void },
97
75
  koaInstrumentation: { install: () => void },
98
- httpInstrumentation: HttpInstrumentation,
76
+ httpInstrumentation: { install: () => void },
99
77
  install: () => void,
100
78
  },
101
79
  inputTracing: {
@@ -15,203 +15,57 @@
15
15
 
16
16
  'use strict';
17
17
 
18
+ const { patchType } = require('../constants');
18
19
  const { Event } = require('@contrast/common');
19
20
 
20
- // Instruments http `Server` and `IncomingMessage` instances to support input
21
- // analysis in framework-agnostic manner.
22
-
23
21
  module.exports = function(core) {
24
- const { protect: { inputAnalysis } } = core;
25
- inputAnalysis.httpInstrumentation = new HttpInstrumentation(core);
26
- return inputAnalysis.httpInstrumentation;
27
- };
28
- class HttpInstrumentation {
29
- constructor(core) {
30
- this.messages = core.messages;
31
- this.scope = core.scopes.sources;
32
- this.config = core.config;
33
- this.logger = core.logger.child('contrast:protect:input-analysis');
34
- this.depHooks = core.depHooks;
35
- this.protect = core.protect;
36
- this.patcher = core.patcher;
37
- this.makeSourceContext = this.protect.makeSourceContext;
38
- this.maxBodySize = 16 * 1024 * 1024;
39
- this.installed = false;
40
- }
41
-
42
- /**
43
- * After checking whether the sensor is enabled, will set up `require` hooks
44
- * for instrumenting both `http` and `https` modules when they load.
45
- */
46
- install() {
47
- if (this.installed) {
48
- return;
49
- }
50
-
51
- this.installed = true;
52
- this.hookHttp();
53
- this.hookHttps();
54
- this.hookHttp2();
55
- }
56
-
57
- uninstall() {
58
- return null; //NYI
59
- }
60
-
61
- /**
62
- * Sets hooks to instrument `http.Server.prototype`.
63
- */
64
- hookHttp() {
65
- this.logger.debug('hooking library: http');
66
- this.depHooks.resolve({ name: 'http' }, (http) => this.hookServerEmit.call(this, http, 'httpServer'));
67
- }
68
-
69
- /**
70
- * Sets hooks to instrument `https.Server.prototype`.
71
- */
72
- hookHttps() {
73
- this.logger.debug('hooking library: https');
74
- this.depHooks.resolve({ name: 'https' }, (https) => this.hookServerEmit.call(this, https, 'httpsServer'));
75
- }
76
-
77
- /**
78
- * Sets hooks to instrument `http2 Servers`.
79
- */
80
- hookHttp2() {
81
- this.logger.debug('hooking library: http2');
82
- // http2 library does not expose its Server class, so we need to hook the createServer function
83
- this.depHooks.resolve({ name: 'http2' }, (http2) => this.hookCreateServer.call(this, http2, 'http2Server'));
84
- this.depHooks.resolve({ name: 'http2' }, (http2) => this.hookCreateServer.call(this, http2, 'http2SecureServer', 'createSecureServer'));
85
-
86
- this.logger.debug('hooking library: spdy');
87
- this.depHooks.resolve({ name: 'spdy' }, (spdy) => this.hookServerEmit.call(this, spdy, 'spdyServer'));
88
- }
89
-
90
- /**
91
- * Instruments the `Server` prototype from `http(s)` or spdy's http2 Server. This patches `emit` and
92
- * invokes the protect service to do analysis when appropriate.
93
- */
94
- hookServerEmit(serverSource, sourceName) {
95
- serverSource.Server.prototype = this.patcher.patch(serverSource.Server.prototype, 'emit', {
96
- name: `${sourceName}.Server.prototype.emit`,
97
- patchType: 'initiate-handling',
98
- around: this.emitAroundHook.bind(this)
99
- });
100
- }
101
-
102
- /**
103
- * Instruments the `Http2Server` prototype which results from the http2.createServer/createSecureServer() call.
104
- * This also patches `emit` and
105
- * invokes the protect service to do analysis when appropriate.
106
- */
107
- hookCreateServer(serverSource, sourceName, constructorName = 'createServer') {
108
- const self = this;
109
-
110
- return this.patcher.patch(serverSource, constructorName, {
111
- name: sourceName,
112
- patchType: 'initiate-handling',
113
- post(data) {
114
-
115
- const { result: server } = data;
116
- const serverPrototype = server ? Object.getPrototypeOf(server) : null;
117
-
118
- if (!serverPrototype) {
119
- self.logger.error('Unable to patch server prototype, continue without instrumentation');
120
- return;
121
- }
122
-
123
- self.patcher.patch(serverPrototype, 'emit', {
124
- name: `${sourceName}.Server.prototype.emit`,
125
- patchType: 'req-async-storage',
126
- around: self.emitAroundHook.bind(self)
127
- });
22
+ const {
23
+ logger,
24
+ messages,
25
+ scopes: { sources },
26
+ instrumentation: { instrument },
27
+ protect: { inputAnalysis },
28
+ } = core;
29
+
30
+ function removeCookies(headers) {
31
+ for (let i = 0; i < headers.length; i += 2) {
32
+ if (headers[i] === 'cookies') {
33
+ headers = headers.slice();
34
+ headers.splice(i, 2);
128
35
  }
129
- });
36
+ }
37
+ return headers;
130
38
  }
131
39
 
132
- /**
133
- * The around hook for `emit` that
134
- * invokes the protect service to do analysis when appropriate.
135
- */
136
- emitAroundHook(next, data) {
137
- const [type] = data.args;
40
+ function around(next, data) {
41
+ let store, block;
42
+ const { args: [type, req, res] } = data;
138
43
 
139
44
  if (type !== 'request') {
140
45
  return next();
141
46
  }
142
47
 
143
- const context = { instance: data.obj, method: next, args: data.args };
144
- this.initiateRequestHandling(context);
145
- return !!data.obj._events[type];
146
- }
147
-
148
- /**
149
- * Creates the sourceContext for the request and invokes the handler for
150
- * inputs that are present in the 'incomingMessage' object at the time of
151
- * the 'connect' event.
152
- *
153
- * @param {Object} context Function invocation context
154
- */
155
- initiateRequestHandling(fnContext) {
156
- const {
157
- instance,
158
- method,
159
- args,
160
- args: [, req, res]
161
- } = fnContext;
162
-
163
- let store;
164
- let block;
165
-
166
48
  try {
167
- const { messages, protect: { inputAnalysis } } = this; // the functions that do input analysis
168
-
169
- // this must be invoked by the patching code using scope.sources.run({}, ...)
170
- // so that an async context is present.
171
- store = this.scope.getStore();
172
- // nothing can be done if async context is not available.
173
-
49
+ store = sources.getStore();
174
50
  if (!store) {
175
- this.logger.debug('cannot acquire store for initiateRequestHandling()');
176
- setImmediate(() => method.call(instance, ...args));
177
- return;
178
- }
179
- store.protect = this.makeSourceContext(req, res);
180
-
181
- if (store.protect.allowed) {
182
- setImmediate(() => method.call(instance, ...args));
51
+ logger.debug('cannot acquire store for around()');
183
52
  return;
184
53
  }
185
54
 
186
- const { reqData } = store.protect;
55
+ store.protect = core.protect.makeSourceContext(req, res);
56
+ const {
57
+ reqData: { headers, uriPath, method }
58
+ } = store.protect;
187
59
 
188
60
  res.on('finish', () => {
189
- this.protect.inputAnalysis.handleRequestEnd(store.protect);
61
+ inputAnalysis.handleRequestEnd(store.protect);
190
62
  messages.emit(Event.PROTECT, store);
191
63
  });
192
64
 
193
- // don't put inputs in the store; they are a param to each handler. findings
194
- // associated with inputs do go into the store. why not put the inputs
195
- // into the store? after all, the inputs come from the store. mostly because
196
- // they can really add up to a lot of data that isn't going to be used.
197
- //
198
- // how to replace result in resultsList, e.g., queries find something
199
- // but then framework emits parsed queries? does this only matter for
200
- // no-sql? index-lookup or hash?
201
- //
202
- // create inputs for this handler. we defer cookies until the framework
203
- // parses them because there is no way to be certain of their formatting
204
- // and encoding.
205
- //
206
- // the primary reason for this is to avoid passing the incomingMessage,
207
- // req, to all the handlers allowing direct access to it and tightly
208
- // coupling all handlers to an extensive collection of data.
209
65
  const connectInputs = {
210
- headers: HttpInstrumentation.removeCookies(reqData.headers),
211
- uriPath: reqData.uriPath,
212
- rawUrl: req.url,
213
- // TODO AGENT-203 - need to handle method-tampering rule.
214
- method: reqData.method,
66
+ headers: removeCookies(headers),
67
+ uriPath,
68
+ method
215
69
  };
216
70
 
217
71
  if (inputAnalysis.virtualPatchesEvaluators?.length) {
@@ -229,25 +83,61 @@ class HttpInstrumentation {
229
83
 
230
84
  block = block || inputAnalysis.handleConnect(store.protect, connectInputs);
231
85
  } catch (err) {
232
- this.logger.error({ err }, 'Error during input analysis');
86
+ logger.error({ err }, 'Error during input analysis');
233
87
  }
234
88
 
235
89
  if (!block) {
236
- setImmediate(() => method.call(instance, ...args));
90
+ setImmediate(() => next.call(data.obj, ...data.args));
237
91
  } else {
238
92
  store.protect.block(...block);
239
- this.logger.debug({ block }, 'request blocked by not emitting request event');
93
+ logger.debug({ block }, 'request blocked by not emitting request event');
240
94
  }
241
95
  }
242
96
 
243
- static removeCookies(headers) {
244
- for (let i = 0; i < headers.length; i += 2) {
245
- if (headers[i] === 'cookies') {
246
- headers = headers.slice();
247
- headers.splice(i, 2);
248
- return headers;
249
- }
250
- }
251
- return headers;
97
+ function install() {
98
+ [{
99
+ moduleName: 'http'
100
+ },
101
+ {
102
+ moduleName: 'https'
103
+ },
104
+ {
105
+ moduleName: 'spdy'
106
+ },
107
+ {
108
+ moduleName: 'http2',
109
+ patchObjectsProps: [
110
+ {
111
+ methods: ['createServer', 'createSecureServer'],
112
+ patchType,
113
+ patchObjects: [
114
+ {
115
+ name: 'Server.prototype',
116
+ methods: ['emit'],
117
+ patchType,
118
+ around
119
+ }
120
+ ]
121
+ }
122
+ ]
123
+ }].forEach(({ moduleName, patchObjectsProps }) => {
124
+ const patchObjects = patchObjectsProps || [
125
+ {
126
+ name: 'Server.prototype',
127
+ methods: ['emit'],
128
+ patchType,
129
+ around
130
+ }
131
+ ];
132
+ instrument({
133
+ moduleName,
134
+ patchObjects
135
+ });
136
+ });
252
137
  }
253
- }
138
+
139
+ return inputAnalysis.httpInstrumentation = {
140
+ install,
141
+ around
142
+ };
143
+ };
@@ -149,9 +149,8 @@ module.exports = function(core) {
149
149
  if (typeof sinkContext.value === 'object') {
150
150
  traverseKeysAndValues(sinkContext.value, function(path, type, value) {
151
151
  if (type !== 'Key' && !agentLib.isMongoQueryType(value)) return;
152
-
153
- stringFindings = handleStringValue(result, sinkContext.value[value], agentLib);
154
-
152
+ const cmdVal = sinkContext.value[value];
153
+ stringFindings = handleStringValue(result, cmdVal?.['$function']?.body || cmdVal, agentLib);
155
154
  // halt traversal
156
155
  return true;
157
156
  });
@@ -289,9 +288,11 @@ function handleStringValue(result, cmd, agentLib) {
289
288
  if (typeof cmd !== 'string') {
290
289
  return null;
291
290
  }
291
+
292
292
  let findings = null;
293
+ let inputIndex = -1;
294
+ inputIndex = cmd.indexOf(result.value);
293
295
 
294
- const inputIndex = cmd.indexOf(result.value);
295
296
  // if the user input is not in the sink input, there is nothing to do.
296
297
  if (inputIndex === -1) {
297
298
  return findings;
@@ -27,105 +27,49 @@ module.exports = function(core) {
27
27
  protect: { inputTracing }
28
28
  } = core;
29
29
 
30
+ function pre({ args, hooked, orig, name }) {
31
+ if (instrumentation.isLocked()) return;
32
+
33
+ const sourceContext = protect.getSourceContext(name);
34
+ if (!sourceContext) return;
35
+
36
+ for (let i = 0; i < (name === 'vm.runInNewContext' ? 2 : 1); i++) {
37
+ const arg = args[i];
38
+ if (!((arg && isString(arg)) || isNonEmptyObject(arg))) continue;
39
+
40
+ const sinkContext = {
41
+ name,
42
+ value: arg,
43
+ stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
44
+ };
45
+ inputTracing.ssjsInjection(sourceContext, sinkContext);
46
+ }
47
+ }
48
+
30
49
  function install() {
31
50
  depHooks.resolve({ name: 'vm' }, (vm) => {
32
- ['Script', 'createScript', 'runInContext', 'runInThisContext'].forEach(
51
+ [
52
+ 'Script',
53
+ 'createScript',
54
+ 'runInContext',
55
+ 'runInThisContext',
56
+ 'createContext',
57
+ 'runInNewContext'
58
+ ].forEach(
33
59
  (method) => {
34
60
  const name = `vm.${method}`;
35
-
36
61
  patcher.patch(vm, method, {
37
62
  name,
38
63
  patchType,
39
- pre({ args, hooked, name, orig }) {
40
- if (instrumentation.isLocked()) return;
41
-
42
- const sourceContext = protect.getSourceContext(name);
43
- if (!sourceContext) return;
44
-
45
- const codeString = args[0];
46
- if (!codeString || !isString(codeString)) return;
47
-
48
- const sinkContext = {
49
- name,
50
- value: codeString,
51
- stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
52
- };
53
- inputTracing.ssjsInjection(sourceContext, sinkContext);
54
- }
64
+ pre
55
65
  });
56
66
  }
57
67
  );
58
68
 
59
- patcher.patch(vm, 'runInNewContext', {
60
- name: 'vm.runInNewContext',
61
- patchType,
62
- pre: ({ args, hooked, orig }) => {
63
- if (instrumentation.isLocked()) return;
64
-
65
- const sourceContext = protect.getSourceContext('vm.runInNewContext');
66
- if (!sourceContext) return;
67
-
68
- const codeString = args[0];
69
- const envObj = args[1];
70
-
71
- if ((!codeString || !isString(codeString)) && (!isNonEmptyObject(envObj))) return;
72
-
73
- const codeStringSinkContext = (codeString && isString(codeString)) ? {
74
- name: 'vm.runInNewContext',
75
- value: codeString,
76
- stacktraceData: { constructorOpt: hooked, prependFrames: [orig] }
77
- } : null;
78
- const envObjSinkContext = isNonEmptyObject(envObj) ? {
79
- name: 'vm.runInNewContext',
80
- value: envObj,
81
- stacktraceData: { constructorOpt: hooked, prependFrames: [orig] }
82
- } : null;
83
-
84
- codeStringSinkContext && inputTracing.ssjsInjection(sourceContext, codeStringSinkContext);
85
- envObjSinkContext && inputTracing.ssjsInjection(sourceContext, envObjSinkContext);
86
- }
87
- });
88
-
89
- patcher.patch(vm, 'createContext', {
90
- name: 'vm.createContext',
91
- patchType,
92
- pre: ({ args, hooked, orig }) => {
93
- if (instrumentation.isLocked()) return;
94
-
95
- const sourceContext = protect.getSourceContext('vm.createContext');
96
- if (!sourceContext) return;
97
-
98
- const envObj = args[0];
99
- if (!isNonEmptyObject(envObj)) return;
100
-
101
- const sinkContext = {
102
- name: 'vm.createContext',
103
- value: envObj,
104
- stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
105
- };
106
- inputTracing.ssjsInjection(sourceContext, sinkContext);
107
- }
108
- });
109
-
110
69
  patcher.patch(vm.Script.prototype, 'runInNewContext', {
111
70
  name: 'vm.Script.prototype.runInNewContext',
112
71
  patchType,
113
- pre: ({ args, hooked, orig }) => {
114
- if (instrumentation.isLocked()) return;
115
-
116
- const sourceContext = protect.getSourceContext('vm.Script.prototype.runInNewContext');
117
- if (!sourceContext) return;
118
-
119
- const envObj = args[0];
120
- if (!isNonEmptyObject(envObj)) return;
121
-
122
- const sinkContext = {
123
- name: 'vm.Script.prototype.runInNewContext',
124
- value: envObj,
125
- stacktraceData: { constructorOpt: hooked, prependFrames: [orig] },
126
- };
127
- inputTracing.ssjsInjection(sourceContext, sinkContext);
128
- }
72
+ pre
129
73
  });
130
74
  });
131
75
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contrast/protect",
3
- "version": "1.9.1",
3
+ "version": "1.11.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,11 +18,11 @@
18
18
  },
19
19
  "dependencies": {
20
20
  "@contrast/agent-lib": "^5.1.0",
21
- "@contrast/common": "1.2.0",
22
- "@contrast/core": "1.8.1",
23
- "@contrast/esm-hooks": "1.4.1",
21
+ "@contrast/common": "1.3.1",
22
+ "@contrast/core": "1.10.0",
23
+ "@contrast/esm-hooks": "1.6.0",
24
24
  "@contrast/scopes": "1.2.0",
25
25
  "ipaddr.js": "^2.0.1",
26
26
  "semver": "^7.3.7"
27
27
  }
28
- }
28
+ }