@contrast/assess 1.40.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.
- package/lib/dataflow/propagation/install/path/index.test.js +1 -1
- package/lib/dataflow/sinks/install/express/reflected-xss.js +1 -1
- package/lib/dataflow/sinks/install/express/unvalidated-redirect.js +1 -1
- package/lib/dataflow/sources/install/body-parser1.js +0 -1
- package/lib/dataflow/sources/install/body-parser1.test.js +4 -8
- package/lib/dataflow/sources/install/cookie-parser1.js +0 -1
- package/lib/dataflow/sources/install/cookie-parser1.test.js +2 -4
- package/lib/dataflow/sources/install/express/params.js +56 -37
- package/lib/dataflow/sources/install/express/params.test.js +80 -73
- package/lib/dataflow/sources/install/express/parsedUrl.js +45 -28
- package/lib/dataflow/sources/install/express/parsedUrl.test.js +70 -29
- package/lib/dataflow/sources/install/qs6.js +0 -1
- package/lib/dataflow/sources/install/restify/router.js +0 -1
- package/lib/dataflow/sources/install/restify/router.test.js +3 -5
- package/lib/get-source-context.js +33 -12
- package/lib/get-source-context.test.js +33 -5
- package/package.json +7 -7
- package/lib/dataflow/sinks/install/fs-original.js +0 -170
|
@@ -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
|
|
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
|
|
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
|
|
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',
|
|
@@ -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.
|
|
116
|
-
|
|
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.
|
|
214
|
-
|
|
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
|
|
|
@@ -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.
|
|
78
|
-
|
|
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: {
|
|
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
|
|
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
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
43
|
-
|
|
47
|
+
it('does not call `.handle` when `match` returns false', function () {
|
|
48
|
+
Layer.prototype.match.returns(false);
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
|
|
50
|
+
simulateRequestScope(() => {
|
|
51
|
+
Reflect.apply(Layer.prototype.match, layer, ['/bar']);
|
|
47
52
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
expect(core.assess.dataflow.sources.handle).not.to.have.been.called;
|
|
54
|
+
});
|
|
55
|
+
});
|
|
51
56
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
expect(core.assess.dataflow.sources.handle).not.to.have.been.called;
|
|
62
|
+
});
|
|
63
|
+
});
|
|
59
64
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
expect(core.assess.dataflow.sources.handle).not.to.have.been.called;
|
|
70
|
+
});
|
|
71
|
+
});
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
77
|
+
expect(core.assess.dataflow.sources.handle).not.to.have.been.called;
|
|
78
|
+
}, { assess: { policy: null } });
|
|
79
|
+
});
|
|
75
80
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
85
|
+
Reflect.apply(Layer.prototype.match, layer, ['/bar']);
|
|
81
86
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
95
|
-
|
|
99
|
+
simulateRequestScope(() => {
|
|
100
|
+
Reflect.apply(Layer.prototype.match, layer, ['/bar']);
|
|
96
101
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
63
|
+
middlewareFn(req, res, cb);
|
|
64
|
+
|
|
65
|
+
expect(cb).to.have.been.called;
|
|
66
|
+
});
|
|
67
|
+
});
|
|
38
68
|
});
|
|
39
69
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
83
|
+
LayerMock = sinon.stub();
|
|
84
|
+
LayerMock.prototype.handleRequest = sinon.stub();
|
|
45
85
|
|
|
46
|
-
|
|
47
|
-
|
|
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
|
});
|
|
@@ -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.
|
|
31
|
-
expect(core.logger.
|
|
32
|
-
|
|
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
|
|
|
@@ -32,23 +32,44 @@ module.exports = function(core) {
|
|
|
32
32
|
} = core;
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
|
+
* getSourceContext must be used by all instrumentation to make sure that the
|
|
36
|
+
* - assess store is available
|
|
37
|
+
* - assess is enabled for this request
|
|
38
|
+
* - instrumentation is not locked
|
|
39
|
+
*
|
|
40
|
+
* Any code that does not use this function and directly accesses the assess
|
|
41
|
+
* source context will recurse until the stack is blown if the following
|
|
42
|
+
* checks are not correctly made.
|
|
43
|
+
*
|
|
35
44
|
* @param {import('./constants.js').InstrumentationType} type
|
|
36
45
|
* @returns {import('@contrast/assess').SourceContext|null} the assess store
|
|
37
46
|
*/
|
|
38
47
|
return core.assess.getSourceContext = function getSourceContext(type, ...rest) {
|
|
39
48
|
const ctx = sources.getStore()?.assess;
|
|
40
|
-
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
|
|
50
|
+
// the following logging used to be done by the caller, but has been moved
|
|
51
|
+
// here as opposed to overloading `ctx.policy` with a special value so the
|
|
52
|
+
// caller could determine whether no source context was available or the
|
|
53
|
+
// request is being intentionally excluded. A negative of this is that the
|
|
54
|
+
// function name is not available to be included in the log.
|
|
55
|
+
if (!ctx) {
|
|
56
|
+
if (type === InstrumentationType.SOURCE) {
|
|
57
|
+
// because this is a real error, and we don't have the function name
|
|
58
|
+
// that the caller previously logged, we generate a stack trace to
|
|
59
|
+
// capture that information.
|
|
60
|
+
const err = new Error('No source context found');
|
|
61
|
+
core.logger.warn({ err }, 'assess running outside of request scope');
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
// there is a context, but if policy is null then assess is intentionally
|
|
66
|
+
// disabled (i.e., url exclusion or the request is not sampled).
|
|
67
|
+
if (!ctx.policy) {
|
|
68
|
+
core.logger.trace('Assess intentionally disabled for this request');
|
|
69
|
+
return null;
|
|
70
|
+
} else if (instrumentation.isLocked()) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
52
73
|
|
|
53
74
|
switch (type) {
|
|
54
75
|
case InstrumentationType.PROPAGATOR: {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { expect } = require('chai');
|
|
4
4
|
const { Event } = require('@contrast/common');
|
|
5
5
|
const { initAssessFixture } = require('@contrast/test/fixtures');
|
|
6
|
+
const sinon = require('sinon');
|
|
6
7
|
const {
|
|
7
8
|
InstrumentationType: { SOURCE, PROPAGATOR, RULE }
|
|
8
9
|
} = require('./constants');
|
|
@@ -36,8 +37,35 @@ describe('assess getSourceContext', function () {
|
|
|
36
37
|
expect(assessStore.policy.enabledRules).to.have.length.greaterThan(5);
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
it('
|
|
40
|
+
it('return null when not in request scope', function() {
|
|
40
41
|
expect(core.assess.getSourceContext()).to.be.null;
|
|
42
|
+
expect(core.logger.warn.callCount).equal(0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('return null and log when getSourceContext(SOURCE) not in request scope', function() {
|
|
46
|
+
expect(core.assess.getSourceContext(SOURCE)).to.be.null;
|
|
47
|
+
expect(core.logger.warn.callCount).equal(1);
|
|
48
|
+
expect(core.logger.warn.args[0][1]).to.include('outside of request scope');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('return null and log when intentionally disabled (policy === null)', function() {
|
|
52
|
+
simulateRequestScope(() => {
|
|
53
|
+
core.scopes.sources.getStore().assess.policy = null;
|
|
54
|
+
expect(core.assess.getSourceContext()).null;
|
|
55
|
+
// lots of code writes trace logs
|
|
56
|
+
const last = core.logger.trace.lastCall;
|
|
57
|
+
expect(last.args[0]).to.include('Assess intentionally disabled');
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('return null and do not log when instrumentation is locked', function() {
|
|
62
|
+
simulateRequestScope(() => {
|
|
63
|
+
core.scopes.instrumentation.isLocked = sinon.stub().returns(true);
|
|
64
|
+
expect(core.assess.getSourceContext()).null;
|
|
65
|
+
if (core.logger.trace.called) {
|
|
66
|
+
expect(core.logger.trace.lastCall.args[0]).not.include('Assess intentionally disabled');
|
|
67
|
+
}
|
|
68
|
+
});
|
|
41
69
|
});
|
|
42
70
|
|
|
43
71
|
describe('getSourceContext()', function() {
|
|
@@ -48,7 +76,7 @@ describe('assess getSourceContext', function () {
|
|
|
48
76
|
});
|
|
49
77
|
});
|
|
50
78
|
|
|
51
|
-
describe('.getSourceContext(SOURCE
|
|
79
|
+
describe('.getSourceContext(SOURCE)', function() {
|
|
52
80
|
it('returns assess store when max source event threshold is not met', function() {
|
|
53
81
|
simulateRequestScope(() => {
|
|
54
82
|
execStoreAssertions(core.assess.getSourceContext(SOURCE));
|
|
@@ -64,10 +92,10 @@ describe('assess getSourceContext', function () {
|
|
|
64
92
|
});
|
|
65
93
|
});
|
|
66
94
|
|
|
67
|
-
describe('.getSourceContext(PROPAGATOR
|
|
95
|
+
describe('.getSourceContext(PROPAGATOR)', function() {
|
|
68
96
|
it('returns assess store when max propagation event threshold is not met', function() {
|
|
69
97
|
simulateRequestScope(() => {
|
|
70
|
-
execStoreAssertions(core.assess.getSourceContext(
|
|
98
|
+
execStoreAssertions(core.assess.getSourceContext(PROPAGATOR));
|
|
71
99
|
});
|
|
72
100
|
});
|
|
73
101
|
|
|
@@ -80,7 +108,7 @@ describe('assess getSourceContext', function () {
|
|
|
80
108
|
});
|
|
81
109
|
});
|
|
82
110
|
|
|
83
|
-
describe('.getSourceContext(
|
|
111
|
+
describe('.getSourceContext(RULE, ruleId?)', function() {
|
|
84
112
|
it('returns assess store when ruleId is not passed', function() {
|
|
85
113
|
simulateRequestScope(() => {
|
|
86
114
|
execStoreAssertions(core.assess.getSourceContext(RULE));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/assess",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.41.0",
|
|
4
4
|
"description": "Contrast service providing framework-agnostic Assess support",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",
|
|
@@ -19,15 +19,15 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@contrast/common": "1.26.0",
|
|
21
21
|
"@contrast/config": "1.36.0",
|
|
22
|
-
"@contrast/core": "1.41.
|
|
23
|
-
"@contrast/dep-hooks": "1.
|
|
22
|
+
"@contrast/core": "1.41.1",
|
|
23
|
+
"@contrast/dep-hooks": "1.10.0",
|
|
24
24
|
"@contrast/distringuish": "^5.1.0",
|
|
25
|
-
"@contrast/instrumentation": "1.
|
|
25
|
+
"@contrast/instrumentation": "1.20.0",
|
|
26
26
|
"@contrast/logger": "1.14.0",
|
|
27
27
|
"@contrast/patcher": "1.13.0",
|
|
28
|
-
"@contrast/rewriter": "1.17.
|
|
29
|
-
"@contrast/route-coverage": "1.
|
|
30
|
-
"@contrast/scopes": "1.
|
|
28
|
+
"@contrast/rewriter": "1.17.1",
|
|
29
|
+
"@contrast/route-coverage": "1.31.0",
|
|
30
|
+
"@contrast/scopes": "1.11.0",
|
|
31
31
|
"semver": "^7.6.0"
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Copyright: 2024 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
|
-
'use strict';
|
|
16
|
-
const { patchType } = require('../common');
|
|
17
|
-
|
|
18
|
-
const {
|
|
19
|
-
DataflowTag: {
|
|
20
|
-
URL_ENCODED,
|
|
21
|
-
LIMITED_CHARS,
|
|
22
|
-
ALPHANUM_SPACE_HYPHEN,
|
|
23
|
-
SAFE_PATH,
|
|
24
|
-
UNTRUSTED,
|
|
25
|
-
},
|
|
26
|
-
FS_METHODS,
|
|
27
|
-
Rule: { PATH_TRAVERSAL: ruleId },
|
|
28
|
-
isString,
|
|
29
|
-
ArrayPrototypeJoin,
|
|
30
|
-
} = require('@contrast/common');
|
|
31
|
-
const { InstrumentationType: { RULE } } = require('../../../constants');
|
|
32
|
-
|
|
33
|
-
module.exports = function(core) {
|
|
34
|
-
const {
|
|
35
|
-
depHooks,
|
|
36
|
-
patcher,
|
|
37
|
-
assess: {
|
|
38
|
-
inspect, // TODO NODE-3455: remove
|
|
39
|
-
getSourceContext,
|
|
40
|
-
eventFactory: { createSinkEvent },
|
|
41
|
-
dataflow: {
|
|
42
|
-
tracker,
|
|
43
|
-
sinks: { isVulnerable, reportFindings },
|
|
44
|
-
},
|
|
45
|
-
},
|
|
46
|
-
} = core;
|
|
47
|
-
|
|
48
|
-
const safeTags = [
|
|
49
|
-
`excluded:${ruleId}`,
|
|
50
|
-
URL_ENCODED,
|
|
51
|
-
LIMITED_CHARS,
|
|
52
|
-
ALPHANUM_SPACE_HYPHEN,
|
|
53
|
-
SAFE_PATH,
|
|
54
|
-
];
|
|
55
|
-
|
|
56
|
-
function getValues(indices, args) {
|
|
57
|
-
return indices.reduce((acc, idx) => {
|
|
58
|
-
const value = args[idx];
|
|
59
|
-
if (value && isString(value)) acc.push(value);
|
|
60
|
-
return acc;
|
|
61
|
-
}, []);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const pre = (name, method, moduleName = 'fs', fullMethodName = '') => (data) => {
|
|
65
|
-
const { name: methodName, indices } = method;
|
|
66
|
-
if (!getSourceContext(RULE, ruleId)) return;
|
|
67
|
-
|
|
68
|
-
const values = getValues(indices, data.args);
|
|
69
|
-
if (!values.length) return;
|
|
70
|
-
|
|
71
|
-
const args = values.map((v) => {
|
|
72
|
-
const strInfo = tracker.getData(v);
|
|
73
|
-
return {
|
|
74
|
-
value: strInfo ? strInfo.value : v,
|
|
75
|
-
tracked: !!strInfo,
|
|
76
|
-
strInfo
|
|
77
|
-
};
|
|
78
|
-
});
|
|
79
|
-
for (let i = 0; i < values.length; i++) {
|
|
80
|
-
const { strInfo } = args[i];
|
|
81
|
-
|
|
82
|
-
if (!strInfo || !isVulnerable(UNTRUSTED, safeTags, strInfo.tags)) {
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const event = createSinkEvent({
|
|
87
|
-
name,
|
|
88
|
-
moduleName,
|
|
89
|
-
methodName: fullMethodName || methodName,
|
|
90
|
-
context: `${name}(${ArrayPrototypeJoin.call(
|
|
91
|
-
args.map((a) => inspect(a.value)),
|
|
92
|
-
', '
|
|
93
|
-
)})`,
|
|
94
|
-
history: [strInfo],
|
|
95
|
-
object: {
|
|
96
|
-
value: 'fs',
|
|
97
|
-
tracked: false,
|
|
98
|
-
},
|
|
99
|
-
args: args.map(({ value, tracked }) => ({ value, tracked })),
|
|
100
|
-
tags: strInfo.tags,
|
|
101
|
-
source: `P${i}`,
|
|
102
|
-
stacktraceOpts: {
|
|
103
|
-
contructorOpt: data.hooked,
|
|
104
|
-
prependFrames: [data.orig],
|
|
105
|
-
},
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
if (event) {
|
|
109
|
-
reportFindings({
|
|
110
|
-
ruleId,
|
|
111
|
-
sinkEvent: event,
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
core.assess.dataflow.sinks.pathTraversal = {
|
|
118
|
-
install() {
|
|
119
|
-
depHooks.resolve({ name: 'fs' }, (fs) => {
|
|
120
|
-
for (const method of FS_METHODS) {
|
|
121
|
-
// not all methods are available on every OS or Node version.
|
|
122
|
-
if (fs[method.name]) {
|
|
123
|
-
const name = `fs.${method.name}`;
|
|
124
|
-
patcher.patch(fs, method.name, {
|
|
125
|
-
name,
|
|
126
|
-
patchType,
|
|
127
|
-
pre: pre(name, method),
|
|
128
|
-
});
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (method.sync) {
|
|
132
|
-
const syncName = `${method.name}Sync`;
|
|
133
|
-
if (fs[syncName]) {
|
|
134
|
-
const name = `fs.${syncName}`;
|
|
135
|
-
patcher.patch(fs, syncName, {
|
|
136
|
-
name,
|
|
137
|
-
patchType,
|
|
138
|
-
pre: pre(name, method, 'fs', syncName),
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (method.promises && fs.promises && fs.promises[method.name]) {
|
|
144
|
-
const name = `fs.promises.${method.name}`;
|
|
145
|
-
patcher.patch(fs.promises, method.name, {
|
|
146
|
-
name,
|
|
147
|
-
patchType,
|
|
148
|
-
pre: pre(name, method, 'fs.promises'),
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
depHooks.resolve({ name: 'fs/promises' }, (fsPromises) => {
|
|
155
|
-
for (const method of FS_METHODS) {
|
|
156
|
-
if (method.promises && fsPromises[method.name]) {
|
|
157
|
-
const name = `fsPromises.${method.name}`;
|
|
158
|
-
patcher.patch(fsPromises, method.name, {
|
|
159
|
-
name,
|
|
160
|
-
patchType,
|
|
161
|
-
pre: pre(name, method, 'fsPromises'),
|
|
162
|
-
});
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
},
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
return core.assess.dataflow.sinks.pathTraversal;
|
|
170
|
-
};
|