@contrast/assess 1.39.0 → 1.41.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.
@@ -4,7 +4,7 @@ const sinon = require('sinon');
4
4
  const { expect } = require('chai');
5
5
  const { initAssessFixture } = require('@contrast/test/fixtures');
6
6
 
7
- describe('assess dataflow propagation string', function () {
7
+ describe('assess dataflow propagation path', function () {
8
8
  let core, instr;
9
9
 
10
10
  beforeEach(function () {
@@ -72,7 +72,7 @@ module.exports = function(core) {
72
72
  ];
73
73
 
74
74
  reflectedXss.install = function() {
75
- depHooks.resolve({ name: 'express', version: '>=4.0.0 <5.0.0', file: 'lib/response' }, (Response) => {
75
+ depHooks.resolve({ name: 'express', version: '>=4 <6', file: 'lib/response' }, (Response) => {
76
76
  const name = 'Express.Response.send';
77
77
  patcher.patch(Response, 'send', {
78
78
  name,
@@ -67,7 +67,7 @@ module.exports = function(core) {
67
67
  ];
68
68
 
69
69
  unvalidatedRedirect.install = function() {
70
- depHooks.resolve({ name: 'express', version: '>=4.0.0 <5.0.0', file: 'lib/response' }, (Response) => {
70
+ depHooks.resolve({ name: 'express', version: '>=4 <6', file: 'lib/response' }, (Response) => {
71
71
  const name = 'Express.Response.location';
72
72
  patcher.patch(Response, 'location', {
73
73
  name: 'Express.Response.location',
@@ -42,7 +42,6 @@ module.exports = function init(core) {
42
42
  const sourceContext = getSourceContext(SOURCE);
43
43
 
44
44
  if (!sourceContext) {
45
- logger.error({ funcKey: data.funcKey }, 'unable to handle source. Missing `sourceContext`');
46
45
  return next(...args);
47
46
  }
48
47
 
@@ -112,10 +112,8 @@ describe('assess dataflow sources body-parser v1', function () {
112
112
  middleware(req, res, next);
113
113
 
114
114
  expect(core.assess.dataflow.sources.handle).not.to.have.been.called;
115
- expect(core.logger.error).to.have.been.calledWith(
116
- { funcKey: 'assess-dataflow-source:body-parser.bodyParser' },
117
- 'unable to handle source. Missing `sourceContext`'
118
- );
115
+ expect(core.logger.trace.callCount).greaterThan(0);
116
+ expect(core.logger.trace.lastCall.args[0]).includes('Assess intentionally disabled');
119
117
  }, { assess: { policy: null } });
120
118
  });
121
119
 
@@ -210,10 +208,8 @@ describe('assess dataflow sources body-parser v1', function () {
210
208
  middleware(req, res, next);
211
209
 
212
210
  expect(core.assess.dataflow.sources.handle).not.to.have.been.called;
213
- expect(core.logger.error).to.have.been.calledWith(
214
- { funcKey: `assess-dataflow-source:body-parser.${method}.${method}Parser` },
215
- 'unable to handle source. Missing `sourceContext`',
216
- );
211
+ expect(core.logger.trace.callCount).greaterThan(0);
212
+ expect(core.logger.trace.lastCall.args[0]).includes('Assess intentionally disabled');
217
213
  }, { assess: { policy: null } });
218
214
  });
219
215
 
@@ -43,7 +43,6 @@ module.exports = function init(core) {
43
43
  const sourceContext = assess.getSourceContext(SOURCE);
44
44
 
45
45
  if (!sourceContext) {
46
- logger.error({ funcKey }, 'unable to handle source. Missing `sourceContext`');
47
46
  return next(...args);
48
47
  }
49
48
 
@@ -74,10 +74,8 @@ describe('assess dataflow sources cookie-parser v1', function () {
74
74
  middleware(req, res, next);
75
75
 
76
76
  expect(core.assess.dataflow.sources.handle).not.to.have.been.called;
77
- expect(core.logger.error).to.have.been.calledWith(
78
- { funcKey: 'assess-dataflow-source:cookie-parser.cookieParser' },
79
- 'unable to handle source. Missing `sourceContext`',
80
- );
77
+ expect(core.logger.trace.callCount).greaterThan(0);
78
+ expect(core.logger.trace.lastCall.args[0]).includes('Assess intentionally disabled');
81
79
  }, { assess: { policy: null } });
82
80
  });
83
81
 
@@ -24,55 +24,74 @@ module.exports = function init(core) {
24
24
  logger,
25
25
  patcher,
26
26
  depHooks,
27
- assess: { getSourceContext },
27
+ assess: {
28
+ getSourceContext,
29
+ dataflow: { sources }
30
+ },
28
31
  } = core;
29
32
 
33
+ function postHook(name) {
34
+ return function({ obj: layer, result, orig, hooked, funcKey }) {
35
+ // we can exit early if
36
+ // the layer doesn't match the request or
37
+ // the layer doesn't recognize any parameters
38
+ if (
39
+ !result ||
40
+ !layer.keys ||
41
+ layer.keys.length === 0
42
+ ) return;
43
+
44
+ const sourceContext = getSourceContext(SOURCE);
45
+ if (!sourceContext) return;
46
+
47
+ if (sourceContext.parsedParams) {
48
+ logger.trace({ funcKey }, 'values already tracked');
49
+ return;
50
+ }
51
+
52
+ try {
53
+ sources.handle({
54
+ context: 'req.params',
55
+ name,
56
+ inputType: InputType.PARAMETER_VALUE,
57
+ stacktraceOpts: {
58
+ constructorOpt: hooked,
59
+ prependFrames: [orig]
60
+ },
61
+ data: layer.params,
62
+ sourceContext
63
+ });
64
+ sourceContext.parsedParams = true;
65
+ } catch (err) {
66
+ logger.error({ err, funcKey }, 'unable to handle source');
67
+ }
68
+ };
69
+ }
70
+
30
71
  core.assess.dataflow.sources.expressInstrumentation.params = {
31
72
  install() {
32
73
  const name = 'Layer.prototype.match';
33
74
  depHooks.resolve(
34
- { name: 'express', version: '>=4.0.0 <5.0.0', file: 'lib/router/layer.js' },
75
+ { name: 'express', version: '>=4 <5', file: 'lib/router/layer.js' },
35
76
  (Layer) => {
36
77
  patcher.patch(Layer.prototype, 'match', {
37
78
  name,
38
79
  patchType,
39
- post({ obj: layer, result, orig, hooked, funcKey }) {
40
- // we can exit early if
41
- // the layer doesn't match the request or
42
- // the layer doesn't recognize any parameters
43
- if (
44
- !result ||
45
- !layer.keys ||
46
- layer.keys.length === 0
47
- ) return;
48
-
49
- const sourceContext = getSourceContext(SOURCE);
50
- if (!sourceContext) return;
51
-
52
- if (sourceContext.parsedParams) {
53
- logger.trace({ funcKey }, 'values already tracked');
54
- return;
55
- }
56
-
57
- try {
58
- core.assess.dataflow.sources.handle({
59
- context: 'req.params',
60
- name,
61
- inputType: InputType.PARAMETER_VALUE,
62
- stacktraceOpts: {
63
- constructorOpt: hooked,
64
- prependFrames: [orig]
65
- },
66
- data: layer.params,
67
- sourceContext
68
- });
69
- sourceContext.parsedParams = true;
70
- } catch (err) {
71
- logger.error({ err, funcKey }, 'unable to handle source');
72
- }
73
- }
80
+ post: postHook(name)
74
81
  });
82
+ return Layer;
83
+ }
84
+ );
75
85
 
86
+ // Used by Express 5
87
+ depHooks.resolve(
88
+ { name: 'router', version: '>=2 <3', file: 'lib/layer.js' },
89
+ (Layer) => {
90
+ patcher.patch(Layer.prototype, 'match', {
91
+ name,
92
+ patchType,
93
+ post: postHook(name)
94
+ });
76
95
  return Layer;
77
96
  }
78
97
  );
@@ -8,96 +8,103 @@ const { InputType } = require('@contrast/common');
8
8
  describe('assess dataflow sources express params', function () {
9
9
  let core, simulateRequestScope, Layer, layer;
10
10
 
11
- beforeEach(function () {
12
- ({ core, simulateRequestScope } = initAssessFixture());
13
-
14
- sinon.stub(core.assess.dataflow.sources, 'handle');
15
-
16
- const LayerMock = sinon.stub();
17
- LayerMock.prototype.match = sinon.stub().returns(true);
18
-
19
- require('./params')(core).install();
20
- Layer = core.depHooks.resolve.yield(LayerMock)[0];
21
- layer = { keys: ['foo'], params: { foo: 'bar' } };
22
- });
11
+ [
12
+ { name: 'express', version: '>=4 <5', file: 'lib/router/layer.js' },
13
+ { name: 'router', version: '>=2 <3', file: 'lib/layer.js' }
14
+ ].forEach((args) => {
15
+ describe(`Express${args.name === 'express' ? '4' : '5'}`, function() {
16
+ beforeEach(function () {
17
+ ({ core, simulateRequestScope } = initAssessFixture());
18
+
19
+ sinon.stub(core.assess.dataflow.sources, 'handle');
20
+
21
+ const LayerMock = sinon.stub();
22
+ LayerMock.prototype.match = sinon.stub().returns(true);
23
+
24
+ require('./params')(core).install();
25
+ [Layer] = core.depHooks.resolve.withArgs(args).yield(LayerMock);
26
+ layer = { keys: ['foo'], params: { foo: 'bar' } };
27
+ });
23
28
 
24
- it('calls `.handle` when `match` adds params (and keys) to the Layer instance', function () {
25
- simulateRequestScope(() => {
26
- Reflect.apply(Layer.prototype.match, layer, ['/bar']);
27
-
28
- expect(core.assess.dataflow.sources.handle).to.have.been.calledWith({
29
- context: 'req.params',
30
- name: 'Layer.prototype.match',
31
- inputType: InputType.PARAMETER_VALUE,
32
- stacktraceOpts: {
33
- constructorOpt: sinon.match.func,
34
- prependFrames: sinon.match.array,
35
- },
36
- data: layer.params,
37
- sourceContext: core.scopes.sources.getStore().assess
29
+ it('calls `.handle` when `match` adds params (and keys) to the Layer instance', function () {
30
+ simulateRequestScope(() => {
31
+ Reflect.apply(Layer.prototype.match, layer, ['/bar']);
32
+
33
+ expect(core.assess.dataflow.sources.handle).to.have.been.calledWith({
34
+ context: 'req.params',
35
+ name: 'Layer.prototype.match',
36
+ inputType: InputType.PARAMETER_VALUE,
37
+ stacktraceOpts: {
38
+ constructorOpt: sinon.match.func,
39
+ prependFrames: sinon.match.array,
40
+ },
41
+ data: layer.params,
42
+ sourceContext: core.scopes.sources.getStore().assess
43
+ });
44
+ });
38
45
  });
39
- });
40
- });
41
46
 
42
- it('does not call `.handle` when `match` returns false', function () {
43
- Layer.prototype.match.returns(false);
47
+ it('does not call `.handle` when `match` returns false', function () {
48
+ Layer.prototype.match.returns(false);
44
49
 
45
- simulateRequestScope(() => {
46
- Reflect.apply(Layer.prototype.match, layer, ['/bar']);
50
+ simulateRequestScope(() => {
51
+ Reflect.apply(Layer.prototype.match, layer, ['/bar']);
47
52
 
48
- expect(core.assess.dataflow.sources.handle).not.to.have.been.called;
49
- });
50
- });
53
+ expect(core.assess.dataflow.sources.handle).not.to.have.been.called;
54
+ });
55
+ });
51
56
 
52
- it('does not call `.handle` when the layer is missing the expected properties', function () {
53
- simulateRequestScope(() => {
54
- Reflect.apply(Layer.prototype.match, {}, ['/bar']);
57
+ it('does not call `.handle` when the layer is missing the expected properties', function () {
58
+ simulateRequestScope(() => {
59
+ Reflect.apply(Layer.prototype.match, {}, ['/bar']);
55
60
 
56
- expect(core.assess.dataflow.sources.handle).not.to.have.been.called;
57
- });
58
- });
61
+ expect(core.assess.dataflow.sources.handle).not.to.have.been.called;
62
+ });
63
+ });
59
64
 
60
- it('does not call `.handle` when `match` adds no params or keys', function () {
61
- simulateRequestScope(() => {
62
- Reflect.apply(Layer.prototype.match, { keys: [], params: {} }, ['/bar']);
65
+ it('does not call `.handle` when `match` adds no params or keys', function () {
66
+ simulateRequestScope(() => {
67
+ Reflect.apply(Layer.prototype.match, { keys: [], params: {} }, ['/bar']);
63
68
 
64
- expect(core.assess.dataflow.sources.handle).not.to.have.been.called;
65
- });
66
- });
69
+ expect(core.assess.dataflow.sources.handle).not.to.have.been.called;
70
+ });
71
+ });
67
72
 
68
- it('does not handle when there is no assess policy in request context', function () {
69
- simulateRequestScope(() => {
70
- Reflect.apply(Layer.prototype.match, layer, ['/bar']);
73
+ it('does not handle when there is no assess policy in request context', function () {
74
+ simulateRequestScope(() => {
75
+ Reflect.apply(Layer.prototype.match, layer, ['/bar']);
71
76
 
72
- expect(core.assess.dataflow.sources.handle).not.to.have.been.called;
73
- }, { assess: { policy: null } });
74
- });
77
+ expect(core.assess.dataflow.sources.handle).not.to.have.been.called;
78
+ }, { assess: { policy: null } });
79
+ });
75
80
 
76
- it('does not call `.handle` when the values are already tracked', function () {
77
- simulateRequestScope(() => {
78
- core.scopes.sources.getStore().assess.parsedParams = true;
81
+ it('does not call `.handle` when the values are already tracked', function () {
82
+ simulateRequestScope(() => {
83
+ core.scopes.sources.getStore().assess.parsedParams = true;
79
84
 
80
- Reflect.apply(Layer.prototype.match, layer, ['/bar']);
85
+ Reflect.apply(Layer.prototype.match, layer, ['/bar']);
81
86
 
82
- expect(core.assess.dataflow.sources.handle).not.to.have.been.called;
83
- expect(core.logger.trace).to.have.been.calledWith(
84
- { funcKey: 'assess-dataflow-source:Layer.prototype.match' },
85
- 'values already tracked'
86
- );
87
- });
88
- });
87
+ expect(core.assess.dataflow.sources.handle).not.to.have.been.called;
88
+ expect(core.logger.trace).to.have.been.calledWith(
89
+ { funcKey: 'assess-dataflow-source:Layer.prototype.match' },
90
+ 'values already tracked'
91
+ );
92
+ });
93
+ });
89
94
 
90
- it('handles a case with an error in the `.handle` method', function () {
91
- const err = new Error('test');
92
- core.assess.dataflow.sources.handle.throws(err);
95
+ it('handles a case with an error in the `.handle` method', function () {
96
+ const err = new Error('test');
97
+ core.assess.dataflow.sources.handle.throws(err);
93
98
 
94
- simulateRequestScope(() => {
95
- Reflect.apply(Layer.prototype.match, layer, ['/bar']);
99
+ simulateRequestScope(() => {
100
+ Reflect.apply(Layer.prototype.match, layer, ['/bar']);
96
101
 
97
- expect(core.logger.error).to.have.been.calledWith(
98
- { err, funcKey: 'assess-dataflow-source:Layer.prototype.match' },
99
- 'unable to handle source'
100
- );
102
+ expect(core.logger.error).to.have.been.calledWith(
103
+ { err, funcKey: 'assess-dataflow-source:Layer.prototype.match' },
104
+ 'unable to handle source'
105
+ );
106
+ });
107
+ });
101
108
  });
102
109
  });
103
110
  });
@@ -29,10 +29,41 @@ module.exports = function init(core) {
29
29
  patcher,
30
30
  } = core;
31
31
 
32
+ function preHook(name, args) {
33
+ return function(data) {
34
+ const [req] = args || data.args;
35
+
36
+ const sourceContext = getSourceContext(SOURCE);
37
+ if (!sourceContext) return;
38
+
39
+ const sourceInfo = {
40
+ context: 'req._parsedUrl',
41
+ data: req._parsedUrl,
42
+ name,
43
+ sourceContext,
44
+ stacktraceOpts: {
45
+ constructorOpt: data.hooked
46
+ }
47
+ };
48
+
49
+ sources.handle({
50
+ ...sourceInfo,
51
+ inputType: InputType.URI,
52
+ keys: ['href', 'path', 'pathname'],
53
+ });
54
+
55
+ sources.handle({
56
+ ...sourceInfo,
57
+ inputType: InputType.QUERYSTRING,
58
+ keys: ['query', 'search'],
59
+ });
60
+ };
61
+ }
62
+
32
63
  core.assess.dataflow.sources.expressInstrumentation.parsedUrl = {
33
64
  install() {
34
65
  depHooks.resolve(
35
- { name: 'express', version: '>=4.0.0 <5.0.0', file: 'lib/middleware/init.js' },
66
+ { name: 'express', version: '>=4 <5', file: 'lib/middleware/init.js' },
36
67
  /** @param {import('express/lib/middleware/init')} mw */
37
68
  (mw) => {
38
69
  const name = 'express.middleware.init';
@@ -44,36 +75,10 @@ module.exports = function init(core) {
44
75
  name: 'express.middleware.init.expressInit',
45
76
  patchType,
46
77
  pre(data) {
47
- const { args: [req] } = data;
48
78
  patcher.patch(data.args, '2', {
49
79
  name: 'express.middleware.init.expressInit.next',
50
80
  patchType,
51
- pre(data) {
52
- const sourceContext = getSourceContext(SOURCE);
53
- if (!sourceContext) return;
54
-
55
- const sourceInfo = {
56
- context: 'req._parsedUrl',
57
- data: req._parsedUrl,
58
- name,
59
- sourceContext,
60
- stacktraceOpts: {
61
- constructorOpt: data.hooked
62
- }
63
- };
64
-
65
- sources.handle({
66
- ...sourceInfo,
67
- inputType: InputType.URI,
68
- keys: ['href', 'path', 'pathname'],
69
- });
70
-
71
- sources.handle({
72
- ...sourceInfo,
73
- inputType: InputType.QUERYSTRING,
74
- keys: ['query', 'search'],
75
- });
76
- }
81
+ pre: preHook(name, data.args)
77
82
  });
78
83
  }
79
84
  });
@@ -81,6 +86,18 @@ module.exports = function init(core) {
81
86
  });
82
87
  }
83
88
  );
89
+
90
+ // Used by Express 5
91
+ depHooks.resolve(
92
+ { name: 'router', version: '>=2 <3', file: 'lib/layer.js' },
93
+ (Layer) => {
94
+ patcher.patch(Layer.prototype, 'handleRequest', {
95
+ name: 'Layer.prototype.handleRequest',
96
+ patchType,
97
+ pre: preHook('Layer.prototype.handleRequest')
98
+ });
99
+ }
100
+ );
84
101
  }
85
102
  };
86
103
 
@@ -13,38 +13,83 @@ describe('assess dataflow sources express parsedUrl', function () {
13
13
  instrumentation = core.assess.dataflow.sources.expressInstrumentation.parsedUrl;
14
14
  tracker = core.assess.dataflow.tracker;
15
15
  patcher = core.patcher;
16
+ });
16
17
 
17
- req = {};
18
- res = {};
19
- middleware = {
20
- init() {
21
- return function (req, res, next) {
22
- req._parsedUrl = {
23
- href: '/some/path?asdf=jkl',
24
- path: '/some/path?asdf=jkl',
25
- pathname: '/some/path',
26
- query: 'asdf=jkl',
27
- search: '?asdf=jkl;',
18
+ describe('express4', function() {
19
+ beforeEach(function() {
20
+ req = {};
21
+ res = {};
22
+ middleware = {
23
+ init() {
24
+ return function (req, res, next) {
25
+ req._parsedUrl = {
26
+ href: '/some/path?asdf=jkl',
27
+ path: '/some/path?asdf=jkl',
28
+ pathname: '/some/path',
29
+ query: 'asdf=jkl',
30
+ search: '?asdf=jkl;',
31
+ };
32
+ next();
28
33
  };
29
- next();
30
- };
31
- }
32
- };
34
+ }
35
+ };
36
+
37
+ core.depHooks
38
+ .resolve
39
+ .withArgs({ name: 'express', version: '>=4 <5', file: 'lib/middleware/init.js' })
40
+ .yields(middleware);
41
+ });
42
+
43
+ it('hooks init next in order to track req._parsedUrl values', function () {
44
+ instrumentation.install();
45
+ expect(patcher.isContrastHooked(middleware.init)).to.be.true;
46
+
47
+ const middlewareFn = middleware.init('text');
48
+
49
+ simulateRequestScope(() => {
50
+ const cb = sinon.spy(function () {
51
+ [
52
+ 'href',
53
+ 'path',
54
+ 'pathname',
55
+ 'query',
56
+ 'search',
57
+ ].forEach((prop) => {
58
+ const strInfo = tracker.getData(req._parsedUrl[prop]);
59
+ expect(strInfo).to.be.ok;
60
+ });
61
+ });
33
62
 
34
- core.depHooks
35
- .resolve
36
- .withArgs({ name: 'express', version: '>=4.0.0 <5.0.0', file: 'lib/middleware/init.js' })
37
- .yields(middleware);
63
+ middlewareFn(req, res, cb);
64
+
65
+ expect(cb).to.have.been.called;
66
+ });
67
+ });
38
68
  });
39
69
 
40
- it('hooks init next in order to track req._parsedUrl values', function () {
41
- instrumentation.install();
42
- expect(patcher.isContrastHooked(middleware.init)).to.be.true;
70
+ describe('express5', function() {
71
+ let LayerMock;
72
+ beforeEach(function() {
73
+ req = {
74
+ _parsedUrl: {
75
+ href: '/some/path?asdf=jkl',
76
+ path: '/some/path?asdf=jkl',
77
+ pathname: '/some/path',
78
+ query: 'asdf=jkl',
79
+ search: '?asdf=jkl;',
80
+ }
81
+ };
43
82
 
44
- const middlewareFn = middleware.init('text');
83
+ LayerMock = sinon.stub();
84
+ LayerMock.prototype.handleRequest = sinon.stub();
45
85
 
46
- simulateRequestScope(() => {
47
- const cb = sinon.spy(function () {
86
+ core.depHooks.resolve.withArgs({ name: 'router', version: '>=2 <3', file: 'lib/layer.js' }).yields(LayerMock);
87
+ });
88
+
89
+ it('hooks handleRequest in order to track req._parsedUrl values', function () {
90
+ instrumentation.install();
91
+ simulateRequestScope(() => {
92
+ LayerMock.prototype.handleRequest(req);
48
93
  [
49
94
  'href',
50
95
  'path',
@@ -56,10 +101,6 @@ describe('assess dataflow sources express parsedUrl', function () {
56
101
  expect(strInfo).to.be.ok;
57
102
  });
58
103
  });
59
-
60
- middlewareFn(req, res, cb);
61
-
62
- expect(cb).to.have.been.called;
63
104
  });
64
105
  });
65
106
  });
@@ -41,7 +41,6 @@ module.exports = (core) => {
41
41
  const sourceContext = getSourceContext(SOURCE);
42
42
 
43
43
  if (!sourceContext) {
44
- logger.error({ inputType, funcKey }, 'unable to handle source. Missing `sourceContext`');
45
44
  return;
46
45
  }
47
46
 
@@ -42,7 +42,6 @@ module.exports = function init(core) {
42
42
  const sourceContext = getSourceContext(SOURCE);
43
43
 
44
44
  if (!sourceContext) {
45
- logger.error({ funcKey }, 'unable to handle source. Missing `sourceContext`');
46
45
  return;
47
46
  }
48
47
 
@@ -27,11 +27,9 @@ describe('assess dataflow sources restify router', function () {
27
27
  simulateRequestScope(() => {
28
28
  Router.prototype.lookup(req);
29
29
 
30
- expect(handle).not.to.have.been.called;
31
- expect(core.logger.error).to.have.been.calledWith(
32
- { funcKey: 'assess-dataflow-source:restify.Router.prototype.lookup' },
33
- 'unable to handle source. Missing `sourceContext`'
34
- );
30
+ expect(handle).not.called;
31
+ expect(core.logger.trace.callCount).greaterThan(0);
32
+ expect(core.logger.trace.lastCall.args[0]).includes('Assess intentionally disabled');
35
33
  }, { assess: { policy: null } });
36
34
  });
37
35