@contrast/assess 1.28.1 → 1.29.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/crypto-analysis/install/crypto.js +2 -2
- package/lib/dataflow/propagation/install/JSON/parse-fn.js +5 -5
- package/lib/dataflow/propagation/install/JSON/parse.js +1 -1
- package/lib/dataflow/propagation/install/JSON/stringify.js +17 -9
- package/lib/dataflow/propagation/install/array-prototype-join.js +7 -6
- package/lib/dataflow/propagation/install/buffer.js +60 -2
- package/lib/dataflow/propagation/install/ejs/template.js +3 -3
- package/lib/dataflow/propagation/install/joi/boolean.js +3 -1
- package/lib/dataflow/propagation/install/joi/expression.js +3 -1
- package/lib/dataflow/propagation/install/joi/keys.js +5 -4
- package/lib/dataflow/propagation/install/joi/number.js +3 -1
- package/lib/dataflow/propagation/install/joi/string-schema.js +1 -5
- package/lib/dataflow/propagation/install/joi/utils.js +9 -5
- package/lib/dataflow/propagation/install/joi/values.js +3 -6
- package/lib/dataflow/propagation/install/mongoose/schema-map.js +2 -2
- package/lib/dataflow/propagation/install/mongoose/schema-mixed.js +2 -2
- package/lib/dataflow/propagation/install/mongoose/schema-string.js +2 -2
- package/lib/dataflow/propagation/install/path/basename.js +2 -2
- package/lib/dataflow/propagation/install/path/common.js +5 -5
- package/lib/dataflow/propagation/install/path/format.js +2 -2
- package/lib/dataflow/propagation/install/path/join-and-resolve.js +2 -2
- package/lib/dataflow/propagation/install/querystring/parse.js +4 -3
- package/lib/dataflow/propagation/install/send.js +2 -2
- package/lib/dataflow/propagation/install/string/concat.js +3 -3
- package/lib/dataflow/propagation/install/string/index.js +3 -2
- package/lib/dataflow/propagation/install/string/match-all.js +0 -1
- package/lib/dataflow/propagation/install/string/match.js +2 -2
- package/lib/dataflow/propagation/install/string/replace.js +6 -6
- package/lib/dataflow/propagation/install/string/slice.js +2 -2
- package/lib/dataflow/propagation/install/string/split.js +2 -2
- package/lib/dataflow/propagation/install/string/substring.js +2 -2
- package/lib/dataflow/sinks/index.js +1 -0
- package/lib/dataflow/sinks/install/child-process.js +3 -3
- package/lib/dataflow/sinks/install/fs.js +2 -2
- package/lib/dataflow/sinks/install/function.js +2 -2
- package/lib/dataflow/sinks/install/restify.js +208 -0
- package/lib/dataflow/sinks/install/vm.js +4 -4
- package/lib/dataflow/sources/handler.js +2 -2
- package/lib/dataflow/sources/index.js +1 -0
- package/lib/dataflow/sources/install/http.js +4 -4
- package/lib/dataflow/sources/install/restify/fieldedTextBodyParser.js +85 -0
- package/lib/dataflow/sources/install/restify/index.js +32 -0
- package/lib/dataflow/sources/install/restify/jsonBodyParser.js +109 -0
- package/lib/dataflow/sources/install/restify/router.js +77 -0
- package/lib/dataflow/tag-utils.js +4 -4
- package/lib/dataflow/tracker.js +1 -0
- package/lib/event-factory.js +3 -3
- package/lib/get-policy.js +2 -2
- package/lib/index.d.ts +18 -0
- package/lib/make-source-context.js +2 -2
- package/lib/response-scanning/handlers/index.js +10 -10
- package/lib/response-scanning/handlers/utils.js +19 -12
- package/lib/response-scanning/install/http.js +9 -59
- package/lib/session-configuration/install/express-session.js +2 -2
- package/lib/session-configuration/install/fastify-cookie.js +2 -2
- package/package.json +4 -4
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
-
const { callChildComponentMethodsSync
|
|
18
|
+
const { callChildComponentMethodsSync } = require('@contrast/common');
|
|
19
|
+
const { StringPrototypeSplit } = require('@contrast/common');
|
|
19
20
|
const { getAdjustedUntrackedValue } = require('../../../tag-utils');
|
|
20
21
|
|
|
21
22
|
module.exports = function(core) {
|
|
@@ -40,7 +41,7 @@ module.exports = function(core) {
|
|
|
40
41
|
};
|
|
41
42
|
|
|
42
43
|
function patchCustomMatcher(matcherFn, objInfo, methodArg, name, patchType) {
|
|
43
|
-
const [, , methodName] =
|
|
44
|
+
const [, , methodName] = StringPrototypeSplit.call(name, '.');
|
|
44
45
|
|
|
45
46
|
return patcher.patch(matcherFn, {
|
|
46
47
|
name,
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
|
-
const {
|
|
17
|
+
const { ArrayPrototypeJoin } = require('@contrast/common');
|
|
18
18
|
const { patchType } = require('../../common');
|
|
19
19
|
const { createSubsetTags, getAdjustedUntrackedValue } = require('../../../tag-utils');
|
|
20
20
|
|
|
@@ -59,7 +59,7 @@ module.exports = function(core) {
|
|
|
59
59
|
args,
|
|
60
60
|
tags,
|
|
61
61
|
result: {
|
|
62
|
-
value:
|
|
62
|
+
value: ArrayPrototypeJoin.call(result),
|
|
63
63
|
tracked: false,
|
|
64
64
|
},
|
|
65
65
|
stacktraceOpts: {
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
|
|
18
18
|
const {
|
|
19
19
|
DataflowTag: { UNTRUSTED },
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
StringPrototypeMatch,
|
|
21
|
+
ArrayPrototypeJoin,
|
|
22
|
+
StringPrototypeSubstring,
|
|
23
23
|
} = require('@contrast/common');
|
|
24
24
|
const { InstrumentationType: { PROPAGATOR } } = require('../../../../constants');
|
|
25
25
|
const {
|
|
@@ -73,7 +73,7 @@ module.exports = function(core) {
|
|
|
73
73
|
replace: str.substring(str.indexOf(match) + match.length, str.length)
|
|
74
74
|
}
|
|
75
75
|
].forEach(({ regex, replace }) => {
|
|
76
|
-
if (ret &&
|
|
76
|
+
if (ret && StringPrototypeMatch.call(ret, regex)) {
|
|
77
77
|
// If the match string is tracked, we can actually use the patched replace
|
|
78
78
|
// to keep track of its tag ranges
|
|
79
79
|
if (tracker.getData(replace)) {
|
|
@@ -87,7 +87,7 @@ module.exports = function(core) {
|
|
|
87
87
|
const numberedGroupMatches = replacementType !== 'function' && replacement.match(/\$[1-9][0-9]|\$[1-9]/g);
|
|
88
88
|
if (numberedGroupMatches) {
|
|
89
89
|
numberedGroupMatches.forEach((numberedGroup) => {
|
|
90
|
-
const group = Number(
|
|
90
|
+
const group = Number(StringPrototypeSubstring.call(numberedGroup, 1));
|
|
91
91
|
ret = origReplace.call(ret, numberedGroup, captureGroups[group - 1] || '');
|
|
92
92
|
});
|
|
93
93
|
}
|
|
@@ -187,7 +187,7 @@ module.exports = function(core) {
|
|
|
187
187
|
name,
|
|
188
188
|
moduleName: 'String',
|
|
189
189
|
methodName: 'prototype.replace',
|
|
190
|
-
context: `'${obj}'.replace(${
|
|
190
|
+
context: `'${obj}'.replace(${ArrayPrototypeJoin.call(args.map(a => a.value))})`,
|
|
191
191
|
history: Array.from(data._history),
|
|
192
192
|
object: {
|
|
193
193
|
value: obj,
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* way not consistent with the End User License Agreement.
|
|
14
14
|
*/
|
|
15
15
|
'use strict';
|
|
16
|
-
const {
|
|
16
|
+
const { ArrayPrototypeJoin } = require('@contrast/common');
|
|
17
17
|
const { InstrumentationType: { PROPAGATOR } } = require('../../../../constants');
|
|
18
18
|
const { createSubsetTags, getAdjustedUntrackedValue } = require('../../../tag-utils');
|
|
19
19
|
const { patchType } = require('../../common');
|
|
@@ -80,7 +80,7 @@ module.exports = function(core) {
|
|
|
80
80
|
name,
|
|
81
81
|
moduleName: 'String',
|
|
82
82
|
methodName: 'prototype.slice',
|
|
83
|
-
context: `'${objInfo.value}'.slice(${
|
|
83
|
+
context: `'${objInfo.value}'.slice(${ArrayPrototypeJoin.call(args.map(a => a.value), ', ')})`,
|
|
84
84
|
history: [objInfo],
|
|
85
85
|
object: {
|
|
86
86
|
value: obj,
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
-
const {
|
|
18
|
+
const { ArrayPrototypeJoin } = require('@contrast/common');
|
|
19
19
|
const { InstrumentationType: { PROPAGATOR } } = require('../../../../constants');
|
|
20
20
|
const { createSubsetTags, getAdjustedUntrackedValue } = require('../../../tag-utils');
|
|
21
21
|
const { patchType } = require('../../common');
|
|
@@ -63,7 +63,7 @@ module.exports = function(core) {
|
|
|
63
63
|
name,
|
|
64
64
|
moduleName: 'String',
|
|
65
65
|
methodName: 'prototype.split',
|
|
66
|
-
context: `'${objInfo.value}'.split(${
|
|
66
|
+
context: `'${objInfo.value}'.split(${ArrayPrototypeJoin.call(args.map(a => a.value))})`,
|
|
67
67
|
history: [objInfo],
|
|
68
68
|
object: {
|
|
69
69
|
value: obj,
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
-
const {
|
|
18
|
+
const { ArrayPrototypeJoin } = require('@contrast/common');
|
|
19
19
|
const { InstrumentationType: { PROPAGATOR } } = require('../../../../constants');
|
|
20
20
|
const { createSubsetTags, getAdjustedUntrackedValue } = require('../../../tag-utils');
|
|
21
21
|
const { patchType } = require('../../common');
|
|
@@ -90,7 +90,7 @@ module.exports = function(core) {
|
|
|
90
90
|
name,
|
|
91
91
|
moduleName: 'String',
|
|
92
92
|
methodName: 'prototype.substring',
|
|
93
|
-
context: `'${objInfo.value}'.substring(${
|
|
93
|
+
context: `'${objInfo.value}'.substring(${ArrayPrototypeJoin.call(args.map(a => a.value))})`,
|
|
94
94
|
history: [objInfo],
|
|
95
95
|
object: {
|
|
96
96
|
value: obj,
|
|
@@ -82,6 +82,7 @@ module.exports = function (core) {
|
|
|
82
82
|
require('./install/mysql')(core);
|
|
83
83
|
require('./install/node-serialize')(core);
|
|
84
84
|
require('./install/postgres')(core);
|
|
85
|
+
require('./install/restify')(core);
|
|
85
86
|
require('./install/sequelize')(core);
|
|
86
87
|
require('./install/sqlite3')(core);
|
|
87
88
|
require('./install/vm')(core);
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
'use strict';
|
|
17
17
|
const {
|
|
18
18
|
DataflowTag: { UNTRUSTED },
|
|
19
|
-
|
|
19
|
+
ArrayPrototypeJoin,
|
|
20
20
|
Rule: { CMD_INJECTION: ruleId },
|
|
21
21
|
isString,
|
|
22
22
|
} = require('@contrast/common');
|
|
@@ -81,7 +81,7 @@ module.exports = function(core) {
|
|
|
81
81
|
name,
|
|
82
82
|
moduleName: 'child_process',
|
|
83
83
|
methodName: method,
|
|
84
|
-
context: `child_process.${method}(${
|
|
84
|
+
context: `child_process.${method}(${ArrayPrototypeJoin.call(args.map((a, idx) => idx === 0 ? `'${a.value}'` : a.value), ', ')})`,
|
|
85
85
|
history: [strInfo],
|
|
86
86
|
object: {
|
|
87
87
|
value: 'child_process',
|
|
@@ -167,7 +167,7 @@ module.exports = function(core) {
|
|
|
167
167
|
name,
|
|
168
168
|
moduleName: 'child_process',
|
|
169
169
|
methodName: method,
|
|
170
|
-
context: `child_process.${method}(${
|
|
170
|
+
context: `child_process.${method}(${ArrayPrototypeJoin.call(args.map((a, idx) => idx === 0 ? `'${a.value}'` : a.value), ', ')})`,
|
|
171
171
|
history: [trackedArgs[vulnerableArgIdx]],
|
|
172
172
|
object: {
|
|
173
173
|
value: 'child_process',
|
|
@@ -26,7 +26,7 @@ const {
|
|
|
26
26
|
FS_METHODS,
|
|
27
27
|
Rule: { PATH_TRAVERSAL: ruleId },
|
|
28
28
|
isString,
|
|
29
|
-
|
|
29
|
+
ArrayPrototypeJoin,
|
|
30
30
|
} = require('@contrast/common');
|
|
31
31
|
const { InstrumentationType: { RULE } } = require('../../../constants');
|
|
32
32
|
|
|
@@ -87,7 +87,7 @@ module.exports = function(core) {
|
|
|
87
87
|
name,
|
|
88
88
|
moduleName,
|
|
89
89
|
methodName: fullMethodName || methodName,
|
|
90
|
-
context: `${name}(${
|
|
90
|
+
context: `${name}(${ArrayPrototypeJoin.call(
|
|
91
91
|
args.map((a) => inspect(a.value)),
|
|
92
92
|
', '
|
|
93
93
|
)})`,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
const {
|
|
19
19
|
isString,
|
|
20
|
-
|
|
20
|
+
ArrayPrototypeJoin,
|
|
21
21
|
DataflowTag: {
|
|
22
22
|
UNTRUSTED,
|
|
23
23
|
CUSTOM_ENCODED_TRUST_BOUNDARY_VIOLATION,
|
|
@@ -114,7 +114,7 @@ module.exports = function (core) {
|
|
|
114
114
|
});
|
|
115
115
|
const event = createSinkEvent({
|
|
116
116
|
name,
|
|
117
|
-
context: `${name}(${
|
|
117
|
+
context: `${name}(${ArrayPrototypeJoin.call(
|
|
118
118
|
args.map((a) => a.inspectedValue),
|
|
119
119
|
', '
|
|
120
120
|
)})`,
|
|
@@ -0,0 +1,208 @@
|
|
|
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
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
Rule: { UNVALIDATED_REDIRECT: ruleId },
|
|
20
|
+
DataflowTag: {
|
|
21
|
+
UNTRUSTED,
|
|
22
|
+
CUSTOM_ENCODED,
|
|
23
|
+
CUSTOM_VALIDATED,
|
|
24
|
+
HTML_ENCODED,
|
|
25
|
+
LIMITED_CHARS,
|
|
26
|
+
URL_ENCODED,
|
|
27
|
+
},
|
|
28
|
+
isString,
|
|
29
|
+
ArrayPrototypeJoin,
|
|
30
|
+
} = require('@contrast/common');
|
|
31
|
+
const { InstrumentationType: { RULE } } = require('../../../constants');
|
|
32
|
+
const { createAppendTags } = require('../../tag-utils');
|
|
33
|
+
const { patchType } = require('../common');
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {{
|
|
37
|
+
* assess: import('@contrast/assess').Assess,
|
|
38
|
+
* config: import('@contrast/config').Config,
|
|
39
|
+
* logger: import('@contrast/logger').Logger,
|
|
40
|
+
* }} core
|
|
41
|
+
* @returns {import('@contrast/common').Installable}
|
|
42
|
+
*/
|
|
43
|
+
module.exports = function(core) {
|
|
44
|
+
const {
|
|
45
|
+
depHooks,
|
|
46
|
+
patcher,
|
|
47
|
+
assess: {
|
|
48
|
+
getSourceContext,
|
|
49
|
+
eventFactory: { createSinkEvent },
|
|
50
|
+
dataflow: {
|
|
51
|
+
tracker,
|
|
52
|
+
sinks: { isVulnerable, reportFindings /*reportSafePositive*/ }
|
|
53
|
+
},
|
|
54
|
+
inspect,
|
|
55
|
+
},
|
|
56
|
+
} = core;
|
|
57
|
+
|
|
58
|
+
const OPTION_FIELDS = ['hostname', 'pathname'];
|
|
59
|
+
const SAFE_TAGS = [
|
|
60
|
+
`excluded:${ruleId}`,
|
|
61
|
+
CUSTOM_ENCODED,
|
|
62
|
+
CUSTOM_VALIDATED,
|
|
63
|
+
HTML_ENCODED,
|
|
64
|
+
LIMITED_CHARS,
|
|
65
|
+
URL_ENCODED,
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
function createCommonEventData(data) {
|
|
69
|
+
return {
|
|
70
|
+
name: 'restify.Response.redirect',
|
|
71
|
+
moduleName: 'restify',
|
|
72
|
+
methodName: 'Response.redirect',
|
|
73
|
+
object: {
|
|
74
|
+
tracked: false,
|
|
75
|
+
value: 'restify.Response',
|
|
76
|
+
},
|
|
77
|
+
result: {
|
|
78
|
+
tracked: false,
|
|
79
|
+
value: undefined,
|
|
80
|
+
},
|
|
81
|
+
stacktraceOpts: {
|
|
82
|
+
constructorOpt: data.hooked,
|
|
83
|
+
prependFrames: [data.orig]
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Called when tracked values are on options object argument.
|
|
90
|
+
* Will return the args coerced to strings values, and the adjusted ranges.
|
|
91
|
+
* @returns {}
|
|
92
|
+
*/
|
|
93
|
+
function getAdjustedValues(origArgs, vulns, vulnArgIdx) {
|
|
94
|
+
let result = '{...';
|
|
95
|
+
let tags;
|
|
96
|
+
|
|
97
|
+
vulns.forEach((vuln, i) => {
|
|
98
|
+
const sep = i == 0 ? '' : ',';
|
|
99
|
+
result += `${sep}'${vuln.path[vuln.path.length - 1]}':'`;
|
|
100
|
+
tags = createAppendTags(tags, vuln.strInfo.tags, result.length);
|
|
101
|
+
result += `${vuln.strInfo.value}'`;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
result += '...}';
|
|
105
|
+
|
|
106
|
+
const args = origArgs.map((a, i) => i == vulnArgIdx ?
|
|
107
|
+
{ tracked: true, value: result } :
|
|
108
|
+
{ tracked: false, value: a?.constructor?.name || typeof a });
|
|
109
|
+
|
|
110
|
+
return { args, tags };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return core.assess.dataflow.sinks.restify = {
|
|
114
|
+
install() {
|
|
115
|
+
// restify adds functionality to the built-in response via this patch function.
|
|
116
|
+
// once it returns the request, it'll have been decorated with redirect() method.
|
|
117
|
+
depHooks.resolve({ name: 'restify', file: 'lib/response.js' }, (responsePatch) => patcher.patch(responsePatch, {
|
|
118
|
+
name: 'restify.response.patch',
|
|
119
|
+
patchType,
|
|
120
|
+
post(data) {
|
|
121
|
+
patcher.patch(data.args[0].prototype, 'redirect', {
|
|
122
|
+
patchType,
|
|
123
|
+
name: 'restify.Response.redirect',
|
|
124
|
+
pre(data) {
|
|
125
|
+
if (!getSourceContext(RULE, ruleId)) return;
|
|
126
|
+
|
|
127
|
+
let vulnArgIdx;
|
|
128
|
+
let vulnArgIsString = true;
|
|
129
|
+
|
|
130
|
+
if (isString(data.args[0])) {
|
|
131
|
+
vulnArgIdx = 0; // res.redirect(url, next)
|
|
132
|
+
} else if (isString(data.args[1])) {
|
|
133
|
+
vulnArgIdx = 1; // res.redirect(code, url, next)
|
|
134
|
+
} else if (data.args[0] && typeof data.args[0] === 'object') {
|
|
135
|
+
vulnArgIdx = 0; // res.redirect(options, next)
|
|
136
|
+
vulnArgIsString = false;
|
|
137
|
+
} else {
|
|
138
|
+
return; // unknown call signature
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (vulnArgIsString) {
|
|
142
|
+
const strInfo = tracker.getData(data.args[vulnArgIdx]);
|
|
143
|
+
if (strInfo && isVulnerable(UNTRUSTED, SAFE_TAGS, strInfo.tags)) {
|
|
144
|
+
const args = data.args.map((arg, idx) => ({
|
|
145
|
+
tracked: idx === vulnArgIdx,
|
|
146
|
+
value: idx === vulnArgIdx ?
|
|
147
|
+
inspect(strInfo.value) :
|
|
148
|
+
(arg?.constructor?.name ?? typeof strInfo.value)
|
|
149
|
+
}));
|
|
150
|
+
|
|
151
|
+
const sinkEvent = createSinkEvent({
|
|
152
|
+
args,
|
|
153
|
+
context: `res.redirect(${ArrayPrototypeJoin.call(args.map((a) => a.value))})`,
|
|
154
|
+
history: [strInfo],
|
|
155
|
+
tags: createAppendTags(null, strInfo.tags, 1), // offset by 1 for formatting as string
|
|
156
|
+
source: `P${vulnArgIdx}`,
|
|
157
|
+
...createCommonEventData(data),
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
if (sinkEvent) {
|
|
161
|
+
reportFindings({ ruleId, sinkEvent });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
const options = data.args[vulnArgIdx];
|
|
166
|
+
const vulns = [];
|
|
167
|
+
|
|
168
|
+
for (const option of OPTION_FIELDS) {
|
|
169
|
+
const strInfo = tracker.getData(options?.[option]);
|
|
170
|
+
if (strInfo && isVulnerable(UNTRUSTED, SAFE_TAGS, strInfo.tags)) {
|
|
171
|
+
vulns.push({ path: [option], strInfo });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (typeof options.query == 'object') {
|
|
176
|
+
for (const [key, value] of Object.entries(options.query)) {
|
|
177
|
+
const strInfo = tracker.getData(value);
|
|
178
|
+
if (strInfo && isVulnerable(UNTRUSTED, SAFE_TAGS, strInfo.tags)) {
|
|
179
|
+
vulns.push({ path: ['query', key], strInfo });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (vulns.length) {
|
|
185
|
+
// so events are not duplicated
|
|
186
|
+
const history = Array.from(new Set(vulns.map((v) => v.strInfo)));
|
|
187
|
+
const { tags, args } = getAdjustedValues(data.args, vulns, vulnArgIdx);
|
|
188
|
+
const sinkEvent = createSinkEvent({
|
|
189
|
+
args,
|
|
190
|
+
context: `res.redirect(${ArrayPrototypeJoin.call(args.map((a) => a.value))})`,
|
|
191
|
+
history,
|
|
192
|
+
tags,
|
|
193
|
+
source: 'P0',
|
|
194
|
+
...createCommonEventData(data),
|
|
195
|
+
|
|
196
|
+
});
|
|
197
|
+
if (sinkEvent) {
|
|
198
|
+
reportFindings({ ruleId, sinkEvent });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}));
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
};
|
|
@@ -27,8 +27,8 @@ const {
|
|
|
27
27
|
Rule: { UNSAFE_CODE_EXECUTION: ruleId },
|
|
28
28
|
isNonEmptyObject,
|
|
29
29
|
isString,
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
ArrayPrototypeJoin,
|
|
31
|
+
StringPrototypeSplit,
|
|
32
32
|
traverseValues,
|
|
33
33
|
} = require('@contrast/common');
|
|
34
34
|
const { InstrumentationType: { RULE } } = require('../../../constants');
|
|
@@ -144,7 +144,7 @@ module.exports = function (core) {
|
|
|
144
144
|
function around(next, { args: origArgs, hooked, orig, name }) {
|
|
145
145
|
if (!getSourceContext(RULE, ruleId) || isLocked(ruleId)) return next();
|
|
146
146
|
|
|
147
|
-
const methodPath =
|
|
147
|
+
const methodPath = StringPrototypeSplit.call(name, '.');
|
|
148
148
|
const method = methodPath[methodPath.length - 1];
|
|
149
149
|
const idxsToCheck = method === 'runInNewContext' ? [0, 1] : [0];
|
|
150
150
|
const argsInfo = accumulateArgsInfo(origArgs, idxsToCheck);
|
|
@@ -204,7 +204,7 @@ module.exports = function (core) {
|
|
|
204
204
|
if (vulnerableArg) {
|
|
205
205
|
const event = createSinkEvent({
|
|
206
206
|
name,
|
|
207
|
-
context: `${name}(${
|
|
207
|
+
context: `${name}(${ArrayPrototypeJoin.call(
|
|
208
208
|
argsInfo.map((a) => a.ctxValue),
|
|
209
209
|
', '
|
|
210
210
|
)})`,
|
|
@@ -19,7 +19,7 @@ const {
|
|
|
19
19
|
InputType,
|
|
20
20
|
DataflowTag,
|
|
21
21
|
isString,
|
|
22
|
-
|
|
22
|
+
ArrayPrototypeJoin,
|
|
23
23
|
} = require('@contrast/common');
|
|
24
24
|
|
|
25
25
|
module.exports = function (core) {
|
|
@@ -137,7 +137,7 @@ module.exports = function (core) {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
traverse(_data, (path, fieldName, value, obj) => {
|
|
140
|
-
const pathName =
|
|
140
|
+
const pathName = ArrayPrototypeJoin.call(path, '.');
|
|
141
141
|
|
|
142
142
|
if (sourceContext.sourceEventsCount >= max) {
|
|
143
143
|
logger.trace({ inputType, sourceName: name }, 'exiting assess source handling - %s max events exceeded', max);
|
|
@@ -26,6 +26,7 @@ module.exports = function (core) {
|
|
|
26
26
|
require('./install/fastify')(core);
|
|
27
27
|
require('./install/hapi')(core);
|
|
28
28
|
require('./install/koa')(core);
|
|
29
|
+
require('./install/restify')(core);
|
|
29
30
|
require('./install/body-parser1')(core);
|
|
30
31
|
require('./install/busboy')(core);
|
|
31
32
|
require('./install/cookie-parser1')(core);
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
const { patchType } = require('../common');
|
|
18
|
-
const {
|
|
18
|
+
const { StringPrototypeToLowerCase, InputType } = require('@contrast/common');
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* @param {{
|
|
@@ -65,13 +65,13 @@ module.exports = function (core) {
|
|
|
65
65
|
const key = obj[i];
|
|
66
66
|
const value = obj[i + 1];
|
|
67
67
|
|
|
68
|
-
if (
|
|
68
|
+
if (StringPrototypeToLowerCase.call(key) === 'content-type') {
|
|
69
69
|
store.assess.responseData.contentType = value;
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
} else if (typeof obj === 'object') {
|
|
73
73
|
for (const [key, value] of Object.entries(obj)) {
|
|
74
|
-
if (
|
|
74
|
+
if (StringPrototypeToLowerCase.call(key) === 'content-type') {
|
|
75
75
|
store.assess.responseData.contentType = value;
|
|
76
76
|
}
|
|
77
77
|
}
|
|
@@ -85,7 +85,7 @@ module.exports = function (core) {
|
|
|
85
85
|
patchType,
|
|
86
86
|
pre(data) {
|
|
87
87
|
const [name = '', value] = data.args;
|
|
88
|
-
if (
|
|
88
|
+
if (StringPrototypeToLowerCase.call(name) === 'content-type' && scopes.sources.getStore()?.assess && value) {
|
|
89
89
|
store.assess.responseData.contentType = value;
|
|
90
90
|
}
|
|
91
91
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const { InputType: { BODY } } = require('@contrast/common');
|
|
19
|
+
const { InstrumentationType: { SOURCE } } = require('../../../../constants');
|
|
20
|
+
const { patchType } = require('../../common');
|
|
21
|
+
|
|
22
|
+
module.exports = function init(core) {
|
|
23
|
+
const {
|
|
24
|
+
logger,
|
|
25
|
+
depHooks,
|
|
26
|
+
patcher,
|
|
27
|
+
assess: {
|
|
28
|
+
dataflow: { sources },
|
|
29
|
+
getSourceContext,
|
|
30
|
+
},
|
|
31
|
+
} = core;
|
|
32
|
+
|
|
33
|
+
return core.assess.dataflow.sources.restifyInstrumentation.fieldedTextBodyParser = {
|
|
34
|
+
install() {
|
|
35
|
+
depHooks.resolve(
|
|
36
|
+
{ name: 'restify', file: 'lib/plugins/fieldedTextBodyParser.js', version: '>=8' },
|
|
37
|
+
(fieldedTextBodyParser) => patcher.patch(fieldedTextBodyParser, {
|
|
38
|
+
name: 'restify.plugins.fieldedTextBodyParser',
|
|
39
|
+
patchType,
|
|
40
|
+
post(data) {
|
|
41
|
+
data.result = patcher.patch(data.result, {
|
|
42
|
+
name: 'restify.plugins.fieldedTextBodyParser.parseFieldedText',
|
|
43
|
+
patchType,
|
|
44
|
+
pre(data) {
|
|
45
|
+
const { args: [req, , next], name, funcKey } = data;
|
|
46
|
+
data.args[2] = function contrastNext(...args) {
|
|
47
|
+
const sourceContext = getSourceContext(SOURCE);
|
|
48
|
+
|
|
49
|
+
if (!sourceContext) {
|
|
50
|
+
logger.error({ funcKey }, 'unable to handle source. Missing `sourceContext`');
|
|
51
|
+
return next(...args);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (sourceContext.parsedBody) {
|
|
55
|
+
logger.trace({ funcKey }, 'values already tracked');
|
|
56
|
+
return next(...args);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
sources.handle({
|
|
61
|
+
context: 'req.body',
|
|
62
|
+
data: req.body,
|
|
63
|
+
inputType: BODY,
|
|
64
|
+
name,
|
|
65
|
+
stacktraceOpts: {
|
|
66
|
+
constructorOpt: contrastNext,
|
|
67
|
+
},
|
|
68
|
+
sourceContext,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
sourceContext.parsedBody = true;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
logger.error({ err, funcKey }, 'unable to handle Restify source');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return next(...args);
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}),
|
|
82
|
+
);
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const { callChildComponentMethodsSync } = require('@contrast/common');
|
|
19
|
+
|
|
20
|
+
module.exports = function init(core) {
|
|
21
|
+
core.assess.dataflow.sources.restifyInstrumentation = {
|
|
22
|
+
install() {
|
|
23
|
+
callChildComponentMethodsSync(this, 'install');
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
require('./fieldedTextBodyParser')(core);
|
|
28
|
+
require('./jsonBodyParser')(core);
|
|
29
|
+
require('./router')(core);
|
|
30
|
+
|
|
31
|
+
return core.assess.dataflow.sources.restifyInstrumentation;
|
|
32
|
+
};
|