@contrast/assess 1.2.0 → 1.4.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/index.js +1 -0
- package/lib/dataflow/propagation/install/querystring/index.js +30 -0
- package/lib/dataflow/propagation/install/querystring/parse.js +124 -0
- package/lib/dataflow/propagation/install/string/index.js +4 -3
- package/lib/dataflow/propagation/install/string/match.js +122 -0
- package/lib/dataflow/propagation/install/string/split.js +1 -0
- package/lib/dataflow/{signatures.js → signatures/index.js} +11 -27
- package/lib/dataflow/signatures/mssql.js +49 -0
- package/lib/dataflow/sinks/index.js +2 -1
- package/lib/dataflow/sinks/install/fastify/unvalidated-redirect.js +5 -2
- package/lib/dataflow/sinks/install/mssql.js +123 -0
- package/lib/dataflow/sinks/install/{postgres/index.js → postgres.js} +35 -28
- package/lib/dataflow/sources/install/http.js +5 -33
- package/lib/dataflow/tracker.js +29 -4
- package/lib/response-scanning/install/http.js +117 -15
- package/package.json +2 -2
|
@@ -36,6 +36,7 @@ module.exports = function(core) {
|
|
|
36
36
|
require('./install/pug-runtime-escape')(core);
|
|
37
37
|
require('./install/sql-template-strings')(core);
|
|
38
38
|
require('./install/unescape')(core);
|
|
39
|
+
require('./install/querystring')(core);
|
|
39
40
|
|
|
40
41
|
propagation.install = function() {
|
|
41
42
|
callChildComponentMethodsSync(propagation, 'install');
|
|
@@ -0,0 +1,30 @@
|
|
|
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 { callChildComponentMethodsSync } = require('@contrast/common');
|
|
19
|
+
|
|
20
|
+
module.exports = function(core) {
|
|
21
|
+
const querystringInstrumentation = core.assess.dataflow.propagation.querystringInstrumentation = {
|
|
22
|
+
install() {
|
|
23
|
+
callChildComponentMethodsSync(querystringInstrumentation, 'install');
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
require('./parse')(core);
|
|
28
|
+
|
|
29
|
+
return querystringInstrumentation;
|
|
30
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
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
|
+
const util = require('util');
|
|
18
|
+
const querystring = require('querystring');
|
|
19
|
+
const { patchType } = require('../../common');
|
|
20
|
+
const { createSubsetTags, createAppendTags } = require('../../../tag-utils');
|
|
21
|
+
|
|
22
|
+
module.exports = function(core) {
|
|
23
|
+
const {
|
|
24
|
+
scopes: { sources, instrumentation },
|
|
25
|
+
patcher,
|
|
26
|
+
depHooks,
|
|
27
|
+
assess: {
|
|
28
|
+
dataflow: { tracker, eventFactory: { createPropagationEvent } }
|
|
29
|
+
}
|
|
30
|
+
} = core;
|
|
31
|
+
|
|
32
|
+
function getUnescapeWrapper(data, unescape) {
|
|
33
|
+
const input = data.args[0];
|
|
34
|
+
const { trackingData } = data;
|
|
35
|
+
|
|
36
|
+
function unescapeWrapper(part) {
|
|
37
|
+
let result = unescape(part);
|
|
38
|
+
const start = input.indexOf(part, data.idx);
|
|
39
|
+
const tagRanges = createSubsetTags(trackingData.tags, start, result.length - 1);
|
|
40
|
+
|
|
41
|
+
if (!tagRanges) return result;
|
|
42
|
+
|
|
43
|
+
const resultInfo = tracker.getData(result);
|
|
44
|
+
const event = createPropagationEvent({
|
|
45
|
+
name: data.name,
|
|
46
|
+
history: [trackingData],
|
|
47
|
+
object: {
|
|
48
|
+
value: part,
|
|
49
|
+
isTracked: true,
|
|
50
|
+
},
|
|
51
|
+
args: data.origArgs.map((arg) => {
|
|
52
|
+
const argInfo = tracker.getData(arg);
|
|
53
|
+
return {
|
|
54
|
+
value: argInfo ? argInfo.value : util.inspect(arg),
|
|
55
|
+
isTracked: !!argInfo
|
|
56
|
+
};
|
|
57
|
+
}),
|
|
58
|
+
result: {
|
|
59
|
+
value: result,
|
|
60
|
+
isTracked: !!resultInfo
|
|
61
|
+
},
|
|
62
|
+
tags: tagRanges,
|
|
63
|
+
stacktraceOpts: {
|
|
64
|
+
constructorOpt: data.hooked,
|
|
65
|
+
prependFrames: [data.orig]
|
|
66
|
+
},
|
|
67
|
+
source: 'P',
|
|
68
|
+
target: 'R'
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
if (event) {
|
|
72
|
+
if (resultInfo) {
|
|
73
|
+
event.tags = createAppendTags(event.tags, resultInfo.tags, 0);
|
|
74
|
+
Object.assign(resultInfo, event);
|
|
75
|
+
}
|
|
76
|
+
if (event.tags['url-encoded']) {
|
|
77
|
+
delete event.tags['url-encoded'];
|
|
78
|
+
event.removedTags = ['url-encoded'];
|
|
79
|
+
}
|
|
80
|
+
const { extern } = resultInfo || tracker.track(result, event);
|
|
81
|
+
if (extern) {
|
|
82
|
+
result = extern;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
}
|
|
86
|
+
data.idx = start + part.length;
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
return unescapeWrapper;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return core.assess.dataflow.propagation.querystringInstrumentation.parse = {
|
|
93
|
+
install() {
|
|
94
|
+
depHooks.resolve({ name: 'querystring' }, (module) => {
|
|
95
|
+
['parse', 'decode'].forEach((method) => {
|
|
96
|
+
patcher.patch(module, method, {
|
|
97
|
+
name: `querystring.${method}`,
|
|
98
|
+
patchType,
|
|
99
|
+
pre(data) {
|
|
100
|
+
if (!sources.getStore()?.assess || instrumentation.isLocked()) return;
|
|
101
|
+
const input = data.args[0];
|
|
102
|
+
if (!input) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const trackingData = tracker.getData(input);
|
|
106
|
+
if (!trackingData) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
data.idx = 0;
|
|
111
|
+
data.origArgs = data.args;
|
|
112
|
+
data.trackingData = trackingData;
|
|
113
|
+
|
|
114
|
+
data.args[3] = {
|
|
115
|
+
...data.args[3],
|
|
116
|
+
decodeURIComponent: getUnescapeWrapper(data, data.args?.[3]?.decodeURIComponent || querystring.unescape)
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
};
|
|
@@ -28,12 +28,13 @@ module.exports = function(core) {
|
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
require('./concat')(core);
|
|
31
|
+
require('./format-methods')(core);
|
|
32
|
+
require('./html-methods')(core);
|
|
33
|
+
require('./match')(core);
|
|
31
34
|
require('./replace')(core);
|
|
35
|
+
require('./split')(core);
|
|
32
36
|
require('./substring')(core);
|
|
33
37
|
require('./trim')(core);
|
|
34
|
-
require('./html-methods')(core);
|
|
35
|
-
require('./format-methods')(core);
|
|
36
|
-
require('./split')(core);
|
|
37
38
|
|
|
38
39
|
return stringInstrumentation;
|
|
39
40
|
};
|
|
@@ -0,0 +1,122 @@
|
|
|
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
|
+
const { join } = require('@contrast/common');
|
|
18
|
+
const { patchType } = require('../../common');
|
|
19
|
+
const { createSubsetTags } = require('../../../tag-utils');
|
|
20
|
+
|
|
21
|
+
module.exports = function(core) {
|
|
22
|
+
const {
|
|
23
|
+
scopes: { sources, instrumentation },
|
|
24
|
+
patcher,
|
|
25
|
+
assess: {
|
|
26
|
+
dataflow: { tracker, eventFactory: { createPropagationEvent } }
|
|
27
|
+
}
|
|
28
|
+
} = core;
|
|
29
|
+
|
|
30
|
+
function getPropagationEvent(data, res, objInfo, start) {
|
|
31
|
+
const { name, args, result, hooked, orig } = data;
|
|
32
|
+
const tags = createSubsetTags(objInfo.tags, start, res.length - 1);
|
|
33
|
+
if (!tags) return;
|
|
34
|
+
|
|
35
|
+
return createPropagationEvent({
|
|
36
|
+
name,
|
|
37
|
+
history: [objInfo],
|
|
38
|
+
object: {
|
|
39
|
+
value: objInfo.value,
|
|
40
|
+
isTracked: true,
|
|
41
|
+
},
|
|
42
|
+
args: args.map((arg) => {
|
|
43
|
+
const argInfo = tracker.getData(arg);
|
|
44
|
+
return {
|
|
45
|
+
value: argInfo ? argInfo.value : arg.toString(),
|
|
46
|
+
isTracked: !!argInfo
|
|
47
|
+
};
|
|
48
|
+
}),
|
|
49
|
+
tags,
|
|
50
|
+
result: {
|
|
51
|
+
value: join(result),
|
|
52
|
+
isTracked: false
|
|
53
|
+
},
|
|
54
|
+
stacktraceOpts: {
|
|
55
|
+
constructorOpt: hooked,
|
|
56
|
+
prependFrames: [orig]
|
|
57
|
+
},
|
|
58
|
+
source: 'O',
|
|
59
|
+
target: 'R'
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return core.assess.dataflow.propagation.stringInstrumentation.match = {
|
|
64
|
+
install() {
|
|
65
|
+
const name = 'String.prototype.match';
|
|
66
|
+
|
|
67
|
+
patcher.patch(String.prototype, 'match', {
|
|
68
|
+
name,
|
|
69
|
+
patchType,
|
|
70
|
+
post(data) {
|
|
71
|
+
const { args, obj, result } = data;
|
|
72
|
+
if (
|
|
73
|
+
!obj ||
|
|
74
|
+
!result ||
|
|
75
|
+
args.length === 0 ||
|
|
76
|
+
result.length === 0 ||
|
|
77
|
+
!sources.getStore() ||
|
|
78
|
+
typeof obj !== 'string' ||
|
|
79
|
+
instrumentation.isLocked() ||
|
|
80
|
+
(args.length === 1 && args[0] == null)
|
|
81
|
+
) return;
|
|
82
|
+
|
|
83
|
+
const objInfo = tracker.getData(obj);
|
|
84
|
+
if (!objInfo) return;
|
|
85
|
+
|
|
86
|
+
let idx = 0;
|
|
87
|
+
const hasCaptureGroups = 'groups' in result;
|
|
88
|
+
for (let i = 0; i < result.length; i++) {
|
|
89
|
+
const res = result[i];
|
|
90
|
+
if (!res) continue;
|
|
91
|
+
const start = obj.indexOf(res, idx);
|
|
92
|
+
idx += hasCaptureGroups ? 0 : res.length;
|
|
93
|
+
const event = getPropagationEvent(data, res, objInfo, start);
|
|
94
|
+
if (event) {
|
|
95
|
+
const { extern } = tracker.track(res, event);
|
|
96
|
+
if (extern) {
|
|
97
|
+
data.result[i] = extern;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (hasCaptureGroups && result.groups) {
|
|
102
|
+
Object.keys(result.groups).forEach((key) => {
|
|
103
|
+
const res = result.groups[key];
|
|
104
|
+
if (!res) return;
|
|
105
|
+
const start = obj.indexOf(res);
|
|
106
|
+
const event = getPropagationEvent(data, res, objInfo, start);
|
|
107
|
+
if (event) {
|
|
108
|
+
const { extern } = tracker.track(res, event);
|
|
109
|
+
if (extern) {
|
|
110
|
+
data.result.groups[key] = extern;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
uninstall() {
|
|
119
|
+
String.prototype.match = patcher.unwrap(String.prototype.match);
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
};
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
module.exports = function(core) {
|
|
19
19
|
const signaturesMap = core.assess.dataflow.signatures = new Map([
|
|
20
|
+
...require('./mssql.js'),
|
|
20
21
|
[
|
|
21
22
|
'Url.prototype.parse',
|
|
22
23
|
{
|
|
@@ -932,6 +933,16 @@ module.exports = function(core) {
|
|
|
932
933
|
target: 'R'
|
|
933
934
|
}
|
|
934
935
|
],
|
|
936
|
+
[
|
|
937
|
+
'String.prototype.match',
|
|
938
|
+
{
|
|
939
|
+
moduleName: 'String',
|
|
940
|
+
methodName: 'prototype.match',
|
|
941
|
+
isModule: false,
|
|
942
|
+
source: 'O',
|
|
943
|
+
target: 'R'
|
|
944
|
+
}
|
|
945
|
+
],
|
|
935
946
|
[
|
|
936
947
|
'sqlite3.Database.prototype.all',
|
|
937
948
|
{
|
|
@@ -986,33 +997,6 @@ module.exports = function(core) {
|
|
|
986
997
|
isModule: true
|
|
987
998
|
}
|
|
988
999
|
],
|
|
989
|
-
[
|
|
990
|
-
'mssql/lib/base/prepared-statement.prototype.prepare',
|
|
991
|
-
{
|
|
992
|
-
moduleName: 'mssql',
|
|
993
|
-
version: '>=6.4.0',
|
|
994
|
-
methodName: 'PreparedStatement.prototype.prepare',
|
|
995
|
-
isModule: true
|
|
996
|
-
}
|
|
997
|
-
],
|
|
998
|
-
[
|
|
999
|
-
'mssql/lib/base/request.prototype.batch',
|
|
1000
|
-
{
|
|
1001
|
-
moduleName: 'mssql',
|
|
1002
|
-
version: '>=6.4.0',
|
|
1003
|
-
methodName: 'Request.prototype.batch',
|
|
1004
|
-
isModule: true
|
|
1005
|
-
}
|
|
1006
|
-
],
|
|
1007
|
-
[
|
|
1008
|
-
'mssql/lib/base/request.prototype.query',
|
|
1009
|
-
{
|
|
1010
|
-
moduleName: 'mssql',
|
|
1011
|
-
version: '>=6.4.0',
|
|
1012
|
-
methodName: 'Request.prototype.query',
|
|
1013
|
-
isModule: true
|
|
1014
|
-
}
|
|
1015
|
-
],
|
|
1016
1000
|
[
|
|
1017
1001
|
'path.format',
|
|
1018
1002
|
{
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
module.exports = [
|
|
19
|
+
[
|
|
20
|
+
'mssql/lib/base/prepared-statement.prototype.prepare',
|
|
21
|
+
{
|
|
22
|
+
moduleName: 'mssql',
|
|
23
|
+
version: '>=6.4.0',
|
|
24
|
+
filename: 'lib/base/prepared-statement.js',
|
|
25
|
+
methodName: 'PreparedStatement.prototype.prepare',
|
|
26
|
+
isModule: true,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
[
|
|
30
|
+
'mssql/lib/base/request.prototype.batch',
|
|
31
|
+
{
|
|
32
|
+
moduleName: 'mssql',
|
|
33
|
+
version: '>=6.4.0',
|
|
34
|
+
filename: 'lib/base/request.js',
|
|
35
|
+
methodName: 'Request.prototype.batch',
|
|
36
|
+
isModule: true,
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
[
|
|
40
|
+
'mssql/lib/base/request.prototype.query',
|
|
41
|
+
{
|
|
42
|
+
moduleName: 'mssql',
|
|
43
|
+
version: '>=6.4.0',
|
|
44
|
+
filename: 'lib/base/request.js',
|
|
45
|
+
methodName: 'Request.prototype.query',
|
|
46
|
+
isModule: true,
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
];
|
|
@@ -32,9 +32,10 @@ module.exports = function (core) {
|
|
|
32
32
|
},
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
+
require('./install/fastify')(core);
|
|
35
36
|
require('./install/http')(core);
|
|
37
|
+
require('./install/mssql')(core);
|
|
36
38
|
require('./install/postgres')(core);
|
|
37
|
-
require('./install/fastify')(core);
|
|
38
39
|
|
|
39
40
|
sinks.install = function() {
|
|
40
41
|
callChildComponentMethodsSync(core.assess.dataflow.sinks, 'install');
|
|
@@ -70,7 +70,10 @@ module.exports = function (core) {
|
|
|
70
70
|
if (isVulnerable(requiredTag, safeTags, strInfo.tags)) {
|
|
71
71
|
const event = createSinkEvent({
|
|
72
72
|
name: 'fastify.reply.redirect',
|
|
73
|
-
object:
|
|
73
|
+
object: {
|
|
74
|
+
value: `[${createModuleLabel('fastify', version)}].Reply`,
|
|
75
|
+
isTracked: false,
|
|
76
|
+
},
|
|
74
77
|
history: [strInfo],
|
|
75
78
|
args: [{
|
|
76
79
|
value: strInfo.value,
|
|
@@ -88,7 +91,7 @@ module.exports = function (core) {
|
|
|
88
91
|
});
|
|
89
92
|
|
|
90
93
|
reportFindings({
|
|
91
|
-
ruleId: 'unvalidated-redirect',
|
|
94
|
+
ruleId: 'unvalidated-redirect', // add Rule.UNVALIDATED_REDIRECT
|
|
92
95
|
metadata: event,
|
|
93
96
|
});
|
|
94
97
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
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 { Rule, isString } = require('@contrast/common');
|
|
19
|
+
const { createModuleLabel } = require('../../propagation/common');
|
|
20
|
+
const { patchType } = require('../common');
|
|
21
|
+
|
|
22
|
+
const SAFE_TAGS = [
|
|
23
|
+
'sql-encoded',
|
|
24
|
+
'limited-chars',
|
|
25
|
+
'custom-validated',
|
|
26
|
+
'custom-encoded',
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
module.exports = function (core) {
|
|
30
|
+
const {
|
|
31
|
+
depHooks,
|
|
32
|
+
patcher,
|
|
33
|
+
scopes: { sources },
|
|
34
|
+
assess: {
|
|
35
|
+
dataflow: {
|
|
36
|
+
tracker,
|
|
37
|
+
sinks: { isVulnerable, reportFindings },
|
|
38
|
+
eventFactory: { createSinkEvent },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
} = core;
|
|
42
|
+
|
|
43
|
+
const pre = (name, obj, version) => (data) => {
|
|
44
|
+
const store = sources.getStore()?.assess;
|
|
45
|
+
if (!store || !data.args[0] || !isString(data.args[0])) return;
|
|
46
|
+
|
|
47
|
+
const strInfo = tracker.getData(data.args[0]);
|
|
48
|
+
if (!strInfo || !isVulnerable('untrusted', SAFE_TAGS, strInfo.tags)) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const event = createSinkEvent({
|
|
53
|
+
name,
|
|
54
|
+
history: [strInfo],
|
|
55
|
+
object: {
|
|
56
|
+
value: `[${createModuleLabel('mssql', version)}].${obj}`,
|
|
57
|
+
isTracked: false,
|
|
58
|
+
},
|
|
59
|
+
args: [
|
|
60
|
+
{
|
|
61
|
+
value: strInfo.value,
|
|
62
|
+
isTracked: true,
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
tags: strInfo.tags,
|
|
66
|
+
source: 'P0',
|
|
67
|
+
stacktraceOpts: {
|
|
68
|
+
contructorOpt: data.hooked,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
reportFindings({
|
|
73
|
+
ruleId: Rule.SQL_INJECTION,
|
|
74
|
+
metadata: event,
|
|
75
|
+
});
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
core.assess.dataflow.sinks.mssql = {
|
|
79
|
+
install() {
|
|
80
|
+
depHooks.resolve(
|
|
81
|
+
{ name: 'mssql', file: 'lib/base/prepared-statement.js' },
|
|
82
|
+
(PreparedStatement, version) => {
|
|
83
|
+
patcher.patch(PreparedStatement.prototype, 'prepare', {
|
|
84
|
+
name: 'PreparedStatement.prototype.prepare',
|
|
85
|
+
patchType,
|
|
86
|
+
pre: pre(
|
|
87
|
+
'mssql/lib/base/prepared-statement.prototype.prepare',
|
|
88
|
+
'PreparedStatement',
|
|
89
|
+
version,
|
|
90
|
+
),
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
depHooks.resolve(
|
|
96
|
+
{ name: 'mssql', file: 'lib/base/request.js' },
|
|
97
|
+
(Request, version) => {
|
|
98
|
+
patcher.patch(Request.prototype, 'batch', {
|
|
99
|
+
name: 'Request.prototype.batch',
|
|
100
|
+
patchType,
|
|
101
|
+
pre: pre(
|
|
102
|
+
'mssql/lib/base/request.prototype.batch',
|
|
103
|
+
'Request',
|
|
104
|
+
version,
|
|
105
|
+
),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
patcher.patch(Request.prototype, 'query', {
|
|
109
|
+
name: 'Request.prototype.query',
|
|
110
|
+
patchType,
|
|
111
|
+
pre: pre(
|
|
112
|
+
'mssql/lib/base/request.prototype.query',
|
|
113
|
+
'Request',
|
|
114
|
+
version,
|
|
115
|
+
),
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
return core.assess.dataflow.sinks.mssql;
|
|
123
|
+
};
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
-
const { isString } = require('@contrast/common');
|
|
19
|
-
const {
|
|
20
|
-
const
|
|
18
|
+
const { Rule, isString } = require('@contrast/common');
|
|
19
|
+
const { createModuleLabel } = require('../../propagation/common');
|
|
20
|
+
const { patchType } = require('../common');
|
|
21
21
|
|
|
22
22
|
module.exports = function (core) {
|
|
23
23
|
const {
|
|
@@ -43,7 +43,7 @@ module.exports = function (core) {
|
|
|
43
43
|
];
|
|
44
44
|
const requiredTag = 'untrusted';
|
|
45
45
|
|
|
46
|
-
const preHook = (methodSignature) => (data) => {
|
|
46
|
+
const preHook = (methodSignature, version, mod, obj) => (data) => {
|
|
47
47
|
const assessStore = sources.getStore()?.assess;
|
|
48
48
|
if (!assessStore) return;
|
|
49
49
|
|
|
@@ -57,16 +57,16 @@ module.exports = function (core) {
|
|
|
57
57
|
const event = createSinkEvent({
|
|
58
58
|
name: methodSignature,
|
|
59
59
|
history: [strInfo],
|
|
60
|
+
object: {
|
|
61
|
+
value: `[${createModuleLabel(mod, version)}].${obj}`,
|
|
62
|
+
isTracked: false,
|
|
63
|
+
},
|
|
60
64
|
args: [
|
|
61
65
|
{
|
|
62
|
-
value:
|
|
66
|
+
value: strInfo.value,
|
|
63
67
|
isTracked: true,
|
|
64
68
|
},
|
|
65
69
|
],
|
|
66
|
-
result: {
|
|
67
|
-
value: data.result,
|
|
68
|
-
isTracked: false,
|
|
69
|
-
},
|
|
70
70
|
tags: strInfo.tags,
|
|
71
71
|
source: 'P0',
|
|
72
72
|
stacktraceOpts: {
|
|
@@ -75,7 +75,7 @@ module.exports = function (core) {
|
|
|
75
75
|
});
|
|
76
76
|
|
|
77
77
|
reportFindings({
|
|
78
|
-
ruleId:
|
|
78
|
+
ruleId: Rule.SQL_INJECTION,
|
|
79
79
|
metadata: event,
|
|
80
80
|
});
|
|
81
81
|
}
|
|
@@ -83,43 +83,50 @@ module.exports = function (core) {
|
|
|
83
83
|
|
|
84
84
|
postgres.install = function () {
|
|
85
85
|
const pgClientQueryPatchName = 'pg.Client.prototype.query';
|
|
86
|
-
depHooks.resolve(
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
86
|
+
depHooks.resolve(
|
|
87
|
+
{ name: 'pg', file: 'lib/client.js' },
|
|
88
|
+
(client, version) => {
|
|
89
|
+
patcher.patch(client.prototype, 'query', {
|
|
90
|
+
name: pgClientQueryPatchName,
|
|
91
|
+
patchType,
|
|
92
|
+
pre: preHook('pg/lib/client.prototype.query', version, 'pg', 'Client'),
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
);
|
|
93
96
|
|
|
94
97
|
const pgNativeClientQueryPatchName = 'pg.native.Client.prototype.query';
|
|
95
|
-
depHooks.resolve(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
depHooks.resolve(
|
|
99
|
+
{ name: 'pg', file: 'lib/native/client.js' },
|
|
100
|
+
(client, version) => {
|
|
101
|
+
patcher.patch(client.prototype, 'query', {
|
|
102
|
+
name: pgNativeClientQueryPatchName,
|
|
103
|
+
patchType,
|
|
104
|
+
pre: preHook('pg/lib/native/client.prototype.query', version, 'pg', 'native.Client'),
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
);
|
|
102
108
|
|
|
103
109
|
const pgClientPatchName = `${patchType}:${pgClientQueryPatchName}.query`;
|
|
104
110
|
const pgNativeClientPatchName = `${patchType}:${pgNativeClientQueryPatchName}.query`;
|
|
105
|
-
depHooks.resolve({ name: 'pg-pool' }, (pool) => {
|
|
111
|
+
depHooks.resolve({ name: 'pg-pool' }, (pool, version) => {
|
|
106
112
|
const name = 'pg-pool.Pool.prototype.query';
|
|
107
113
|
patcher.patch(pool.prototype, 'query', {
|
|
108
114
|
name,
|
|
109
115
|
patchType,
|
|
110
116
|
pre: (data) => {
|
|
111
117
|
const funcKeys = patcher.hooks.get(
|
|
112
|
-
data.obj.Client?.prototype?.query
|
|
118
|
+
data.obj.Client?.prototype?.query,
|
|
113
119
|
)?.funcKeys;
|
|
114
120
|
|
|
115
121
|
if (
|
|
116
122
|
funcKeys &&
|
|
117
|
-
(funcKeys.has(pgClientPatchName) ||
|
|
123
|
+
(funcKeys.has(pgClientPatchName) ||
|
|
124
|
+
funcKeys.has(pgNativeClientPatchName))
|
|
118
125
|
) {
|
|
119
126
|
return;
|
|
120
127
|
}
|
|
121
128
|
|
|
122
|
-
preHook(name)(data);
|
|
129
|
+
preHook(name, version, 'pg-pool', 'Pool')(data);
|
|
123
130
|
},
|
|
124
131
|
});
|
|
125
132
|
});
|
|
@@ -132,43 +132,15 @@ module.exports = function(core) {
|
|
|
132
132
|
}
|
|
133
133
|
|
|
134
134
|
function install() {
|
|
135
|
-
[{
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
moduleName: 'https'
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
moduleName: 'spdy'
|
|
143
|
-
},
|
|
144
|
-
{
|
|
145
|
-
moduleName: 'http2',
|
|
146
|
-
patchObjectsProps: [
|
|
147
|
-
{
|
|
148
|
-
methods: ['createServer', 'createSecureServer'],
|
|
149
|
-
patchType,
|
|
150
|
-
patchObjects: [
|
|
151
|
-
{
|
|
152
|
-
name: 'Server.prototype',
|
|
153
|
-
methods: ['emit'],
|
|
154
|
-
patchType,
|
|
155
|
-
around
|
|
156
|
-
}
|
|
157
|
-
]
|
|
158
|
-
}
|
|
159
|
-
]
|
|
160
|
-
}].forEach(({ moduleName, patchObjectsProps }) => {
|
|
161
|
-
const patchObjects = patchObjectsProps || [
|
|
162
|
-
{
|
|
135
|
+
['http', 'https', 'spdy', 'http2'].forEach((moduleName) => {
|
|
136
|
+
instrument({
|
|
137
|
+
moduleName,
|
|
138
|
+
patchObjects: [{
|
|
163
139
|
name: 'Server.prototype',
|
|
164
140
|
methods: ['emit'],
|
|
165
141
|
patchType,
|
|
166
142
|
around
|
|
167
|
-
}
|
|
168
|
-
];
|
|
169
|
-
instrument({
|
|
170
|
-
moduleName,
|
|
171
|
-
patchObjects
|
|
143
|
+
}]
|
|
172
144
|
});
|
|
173
145
|
});
|
|
174
146
|
}
|
package/lib/dataflow/tracker.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
18
|
const distringuish = require('@contrast/distringuish');
|
|
19
|
+
const { isString } = require('@contrast/common');
|
|
19
20
|
|
|
20
21
|
module.exports = function tracker(core) {
|
|
21
22
|
const {
|
|
@@ -66,11 +67,35 @@ module.exports = function tracker(core) {
|
|
|
66
67
|
return { extern: null };
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (
|
|
70
|
+
let extern = distringuish.externalize(value);
|
|
71
|
+
|
|
72
|
+
if (extern == 2) {
|
|
73
|
+
// Try work-around for some wrong/unknown encoding
|
|
74
|
+
extern = distringuish.externalize(Buffer.from(value).toString());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!extern || !isString(extern)) {
|
|
78
|
+
let errMsg;
|
|
79
|
+
switch (extern) {
|
|
80
|
+
case 1:
|
|
81
|
+
errMsg = 'zero-length string was passed';
|
|
82
|
+
break;
|
|
83
|
+
case 2:
|
|
84
|
+
errMsg = 'non-two-byte encoded string was passed';
|
|
85
|
+
break;
|
|
86
|
+
case 3:
|
|
87
|
+
errMsg = 'distringuish was unable to convert a MaybeLocal to Local';
|
|
88
|
+
break;
|
|
89
|
+
case 4:
|
|
90
|
+
errMsg = 'distinguish\'s isExternal call returned false';
|
|
91
|
+
break;
|
|
92
|
+
default:
|
|
93
|
+
errMsg = 'unknown error while trying to externalize the string was encountered';
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
|
|
72
97
|
const err = new Error();
|
|
73
|
-
logger.error({ err, value },
|
|
98
|
+
logger.error({ err, value }, `tracker.track was unable to externalize because ${errMsg}`);
|
|
74
99
|
return { extern: null };
|
|
75
100
|
}
|
|
76
101
|
|
|
@@ -60,6 +60,27 @@ module.exports = function(core) {
|
|
|
60
60
|
|
|
61
61
|
const patchType = 'response-scanning';
|
|
62
62
|
|
|
63
|
+
function writeHookChecks(sourceContext, evaluationContext) {
|
|
64
|
+
// Check only the rules concerning the response body
|
|
65
|
+
handleAutoCompleteMissing(sourceContext, evaluationContext);
|
|
66
|
+
handleCacheControlsMissing(sourceContext, evaluationContext);
|
|
67
|
+
handleParameterPollution(sourceContext, evaluationContext);
|
|
68
|
+
handleXxsProtectionHeaderDisabled(sourceContext, evaluationContext);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function endHookChecks(sourceContext, evaluationContext) {
|
|
72
|
+
// Check all the response scanning rules
|
|
73
|
+
handleAutoCompleteMissing(sourceContext, evaluationContext);
|
|
74
|
+
handleCacheControlsMissing(sourceContext, evaluationContext);
|
|
75
|
+
handleClickJackingControlsMissing(sourceContext, evaluationContext);
|
|
76
|
+
handleParameterPollution(sourceContext, evaluationContext);
|
|
77
|
+
handleCspHeader(sourceContext, evaluationContext);
|
|
78
|
+
handleHstsHeaderMissing(sourceContext, evaluationContext);
|
|
79
|
+
handlePoweredByHeader(sourceContext, evaluationContext);
|
|
80
|
+
handleXContentTypeHeaderMissing(sourceContext, evaluationContext);
|
|
81
|
+
handleXxsProtectionHeaderDisabled(sourceContext, evaluationContext);
|
|
82
|
+
}
|
|
83
|
+
|
|
63
84
|
http.install = function() {
|
|
64
85
|
depHooks.resolve({ name: 'http' }, (http) => {
|
|
65
86
|
{
|
|
@@ -76,11 +97,7 @@ module.exports = function(core) {
|
|
|
76
97
|
responseHeaders: parseHeaders(data.result._header),
|
|
77
98
|
};
|
|
78
99
|
|
|
79
|
-
|
|
80
|
-
handleAutoCompleteMissing(sourceContext, evaluationContext);
|
|
81
|
-
handleCacheControlsMissing(sourceContext, evaluationContext);
|
|
82
|
-
handleParameterPollution(sourceContext, evaluationContext);
|
|
83
|
-
handleXxsProtectionHeaderDisabled(sourceContext, evaluationContext);
|
|
100
|
+
writeHookChecks(sourceContext, evaluationContext);
|
|
84
101
|
}
|
|
85
102
|
});
|
|
86
103
|
}
|
|
@@ -98,19 +115,104 @@ module.exports = function(core) {
|
|
|
98
115
|
responseHeaders: parseHeaders(data.result._header),
|
|
99
116
|
};
|
|
100
117
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
118
|
+
endHookChecks(sourceContext, evaluationContext);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
depHooks.resolve({ name: 'http2' }, (http2) => {
|
|
125
|
+
// Patching the response object
|
|
126
|
+
{
|
|
127
|
+
const name = 'http2.Http2ServerResponse.prototype.write';
|
|
128
|
+
patcher.patch(http2.Http2ServerResponse.prototype, 'write', {
|
|
129
|
+
name,
|
|
130
|
+
patchType,
|
|
131
|
+
post(data) {
|
|
132
|
+
const sourceContext = sources.getStore()?.assess;
|
|
133
|
+
if (!sourceContext) return;
|
|
134
|
+
|
|
135
|
+
const headersSymbol = Object.getOwnPropertySymbols(data.obj).find(symbol => symbol.toString().includes('headers'));
|
|
136
|
+
const evaluationContext = {
|
|
137
|
+
responseBody: toLowerCase(data.args[0] || ''),
|
|
138
|
+
responseHeaders: data.obj[headersSymbol],
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
writeHookChecks(sourceContext, evaluationContext);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
{
|
|
146
|
+
const name = 'http2.Http2ServerResponse.prototype.end';
|
|
147
|
+
patcher.patch(http2.Http2ServerResponse.prototype, 'end', {
|
|
148
|
+
name,
|
|
149
|
+
patchType,
|
|
150
|
+
post(data) {
|
|
151
|
+
const sourceContext = sources.getStore()?.assess;
|
|
152
|
+
if (!sourceContext) return;
|
|
153
|
+
|
|
154
|
+
const headersSymbol = Object.getOwnPropertySymbols(data.result).find(symbol => symbol.toString().includes('headers'));
|
|
155
|
+
const evaluationContext = {
|
|
156
|
+
responseBody: toLowerCase(data.args[0] || ''),
|
|
157
|
+
responseHeaders: data.result[headersSymbol],
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
endHookChecks(sourceContext, evaluationContext);
|
|
111
161
|
}
|
|
112
162
|
});
|
|
113
163
|
}
|
|
164
|
+
|
|
165
|
+
// patching the stream object
|
|
166
|
+
['createServer', 'createSecureServer'].forEach((server) => {
|
|
167
|
+
patcher.patch(http2, server, {
|
|
168
|
+
name: `http2.${server}`,
|
|
169
|
+
patchType,
|
|
170
|
+
post(data) {
|
|
171
|
+
// The other option is once again to patch the `emit` method of the server
|
|
172
|
+
// similar to how we patch it for creating sources, but take action only on
|
|
173
|
+
// `stream` events. I chose the currentt approach because the hook will only
|
|
174
|
+
// run on a `stream` event instead of checking and returning on each `request`
|
|
175
|
+
// connect.
|
|
176
|
+
data.result._events = patcher.patch(data.result._events, 'stream', {
|
|
177
|
+
name: 'stream',
|
|
178
|
+
patchType: 'stream-patch',
|
|
179
|
+
pre(data) {
|
|
180
|
+
patcher.patch(data.args[0], 'write', {
|
|
181
|
+
name: 'Http2Stream.write',
|
|
182
|
+
patchType,
|
|
183
|
+
post(data) {
|
|
184
|
+
const sourceContext = sources.getStore()?.assess;
|
|
185
|
+
if (!sourceContext) return;
|
|
186
|
+
|
|
187
|
+
const evaluationContext = {
|
|
188
|
+
responseBody: toLowerCase(data.args[0] || ''),
|
|
189
|
+
responseHeaders: data.obj.sentHeaders,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
writeHookChecks(sourceContext, evaluationContext);
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
patcher.patch(data.args[0], 'end', {
|
|
197
|
+
name: 'Http2Stream.end',
|
|
198
|
+
patchType,
|
|
199
|
+
post(data) {
|
|
200
|
+
const sourceContext = sources.getStore()?.assess;
|
|
201
|
+
if (!sourceContext) return;
|
|
202
|
+
|
|
203
|
+
const evaluationContext = {
|
|
204
|
+
responseBody: toLowerCase(data.args[0] || ''),
|
|
205
|
+
responseHeaders: data.obj.sentHeaders,
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
endHookChecks(sourceContext, evaluationContext);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
});
|
|
114
216
|
});
|
|
115
217
|
};
|
|
116
218
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contrast/assess",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@contrast/distringuish": "^4.1.0",
|
|
17
17
|
"@contrast/scopes": "1.3.0",
|
|
18
|
-
"@contrast/common": "1.
|
|
18
|
+
"@contrast/common": "1.7.0",
|
|
19
19
|
"parseurl": "^1.3.3"
|
|
20
20
|
}
|
|
21
21
|
}
|