@contrast/assess 1.5.0 → 1.7.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/event-factory.js +10 -5
- package/lib/dataflow/propagation/index.js +1 -0
- package/lib/dataflow/propagation/install/contrast-methods/string.js +5 -1
- package/lib/dataflow/propagation/install/decode-uri-component.js +5 -2
- package/lib/dataflow/propagation/install/ejs/escape-xml.js +5 -2
- package/lib/dataflow/propagation/install/encode-uri-component.js +5 -2
- package/lib/dataflow/propagation/install/escape-html.js +7 -4
- package/lib/dataflow/propagation/install/escape.js +5 -2
- package/lib/dataflow/propagation/install/handlebars-utils-escape-expression.js +5 -2
- package/lib/dataflow/propagation/install/mysql-connection-escape.js +5 -2
- package/lib/dataflow/propagation/install/pug-runtime-escape.js +5 -2
- package/lib/dataflow/propagation/install/querystring/parse.js +8 -3
- package/lib/dataflow/propagation/install/sequelize.js +310 -0
- package/lib/dataflow/propagation/install/sql-template-strings.js +5 -4
- package/lib/dataflow/propagation/install/string/match.js +2 -2
- package/lib/dataflow/propagation/install/string/replace.js +13 -4
- package/lib/dataflow/propagation/install/unescape.js +5 -2
- package/lib/dataflow/propagation/install/validator/methods.js +60 -51
- package/lib/dataflow/sinks/common.js +10 -1
- package/lib/dataflow/sinks/index.js +34 -1
- package/lib/dataflow/sinks/install/child-process.js +150 -13
- package/lib/dataflow/sinks/install/express/index.js +29 -0
- package/lib/dataflow/sinks/install/express/unvalidated-redirect.js +134 -0
- package/lib/dataflow/sinks/install/fastify/unvalidated-redirect.js +113 -75
- package/lib/dataflow/sinks/install/fs.js +136 -0
- package/lib/dataflow/sinks/install/http.js +46 -17
- package/lib/dataflow/sinks/install/koa/unvalidated-redirect.js +50 -17
- package/lib/dataflow/sinks/install/marsdb.js +135 -0
- package/lib/dataflow/sinks/install/mongodb.js +322 -0
- package/lib/dataflow/sinks/install/mssql.js +19 -10
- package/lib/dataflow/sinks/install/mysql.js +138 -0
- package/lib/dataflow/sinks/install/postgres.js +37 -23
- package/lib/dataflow/sinks/install/sequelize.js +142 -0
- package/lib/dataflow/sinks/install/sqlite3.js +20 -10
- package/lib/dataflow/sources/handler.js +14 -9
- package/lib/dataflow/sources/index.js +4 -1
- package/lib/dataflow/sources/install/body-parser1.js +120 -0
- package/lib/dataflow/sources/install/cookie-parser1.js +101 -0
- package/lib/dataflow/sources/install/express/index.js +28 -0
- package/package.json +3 -3
|
@@ -16,19 +16,50 @@
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
18
|
const util = require('util');
|
|
19
|
-
const {
|
|
20
|
-
|
|
19
|
+
const {
|
|
20
|
+
DataflowTag: {
|
|
21
|
+
UNTRUSTED,
|
|
22
|
+
CUSTOM_ENCODED,
|
|
23
|
+
CUSTOM_VALIDATED,
|
|
24
|
+
HTML_ENCODED,
|
|
25
|
+
LIMITED_CHARS,
|
|
26
|
+
URL_ENCODED,
|
|
27
|
+
},
|
|
28
|
+
isString
|
|
29
|
+
} = require('@contrast/common');
|
|
21
30
|
const { createSubsetTags } = require('../../../tag-utils');
|
|
31
|
+
const { filterSafeTags, patchType } = require('../../common');
|
|
32
|
+
|
|
33
|
+
const ruleId = 'unvalidated-redirect';
|
|
34
|
+
const getURLArgument = (args) => {
|
|
35
|
+
if (!Array.isArray(args)) {
|
|
36
|
+
return { index: null, url: undefined };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// url can be first or second argument
|
|
40
|
+
if (typeof args[0] === 'string') {
|
|
41
|
+
return {
|
|
42
|
+
index: 0,
|
|
43
|
+
url: args[0]
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
index: 1,
|
|
49
|
+
url: args[1]
|
|
50
|
+
};
|
|
51
|
+
};
|
|
22
52
|
|
|
23
53
|
module.exports = function (core) {
|
|
24
54
|
const {
|
|
55
|
+
config,
|
|
25
56
|
depHooks,
|
|
26
57
|
patcher,
|
|
27
58
|
scopes: { sources },
|
|
28
59
|
assess: {
|
|
29
60
|
dataflow: {
|
|
30
61
|
tracker,
|
|
31
|
-
sinks: { isVulnerable, reportFindings },
|
|
62
|
+
sinks: { isVulnerable, reportFindings, reportSafePositive },
|
|
32
63
|
eventFactory: { createSinkEvent },
|
|
33
64
|
},
|
|
34
65
|
},
|
|
@@ -39,85 +70,92 @@ module.exports = function (core) {
|
|
|
39
70
|
const inspect = patcher.unwrap(util.inspect);
|
|
40
71
|
|
|
41
72
|
const safeTags = [
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
73
|
+
CUSTOM_ENCODED,
|
|
74
|
+
CUSTOM_VALIDATED,
|
|
75
|
+
HTML_ENCODED,
|
|
76
|
+
LIMITED_CHARS,
|
|
77
|
+
URL_ENCODED,
|
|
47
78
|
];
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
args
|
|
79
|
+
|
|
80
|
+
unvalidatedRedirect.install = function () {
|
|
81
|
+
const name = 'fastify.Reply.prototype.redirect';
|
|
82
|
+
depHooks.resolve({ name: 'fastify', file: 'lib/reply' }, (Reply, version) => {
|
|
83
|
+
patcher.patch(Reply.prototype, 'redirect', {
|
|
84
|
+
name,
|
|
85
|
+
patchType,
|
|
86
|
+
post(data) {
|
|
87
|
+
const assessStore = sources.getStore()?.assess;
|
|
88
|
+
if (!assessStore) return;
|
|
89
|
+
|
|
90
|
+
const { url, index: valueIndex } = getURLArgument(data.args);
|
|
91
|
+
if (!url || !isString(url)) return;
|
|
92
|
+
|
|
93
|
+
const strInfo = tracker.getData(url);
|
|
94
|
+
if (!strInfo) return;
|
|
95
|
+
|
|
96
|
+
// todo: how does different tag logic play into display ranges?
|
|
97
|
+
|
|
98
|
+
let urlPathTags = strInfo.tags;
|
|
99
|
+
const urlPathEndIdx = url.indexOf('?');
|
|
100
|
+
if (urlPathEndIdx > -1) {
|
|
101
|
+
urlPathTags = createSubsetTags(strInfo.tags, 0, urlPathEndIdx);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (isVulnerable(UNTRUSTED, safeTags, urlPathTags)) {
|
|
105
|
+
const args = [];
|
|
106
|
+
// in case a status code is provided
|
|
107
|
+
if (valueIndex) {
|
|
108
|
+
args.push({
|
|
109
|
+
tracked: false,
|
|
110
|
+
value: data.args[0]
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
args.push({
|
|
83
114
|
tracked: true,
|
|
84
115
|
value: strInfo.value,
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const event = createSinkEvent({
|
|
119
|
+
args,
|
|
120
|
+
context: `reply.redirect(${inspect(strInfo.value)})`,
|
|
121
|
+
history: [strInfo],
|
|
122
|
+
name: 'fastify.reply.redirect',
|
|
123
|
+
object: {
|
|
124
|
+
tracked: false,
|
|
125
|
+
value: 'fastify.Reply',
|
|
126
|
+
},
|
|
127
|
+
result: {
|
|
128
|
+
tracked: false,
|
|
129
|
+
value: undefined,
|
|
130
|
+
},
|
|
131
|
+
tags: urlPathTags,
|
|
132
|
+
source: `P${valueIndex}`,
|
|
133
|
+
stacktraceOpts: {
|
|
134
|
+
constructorOpt: data.hooked,
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (event) {
|
|
139
|
+
reportFindings({
|
|
140
|
+
ruleId,
|
|
141
|
+
sinkEvent: event,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
} else if (config.assess.safe_positives.enable) {
|
|
145
|
+
reportSafePositive({
|
|
146
|
+
name,
|
|
147
|
+
ruleId,
|
|
148
|
+
safeTags: filterSafeTags(safeTags, strInfo),
|
|
149
|
+
strInfo: {
|
|
150
|
+
tags: strInfo.tags,
|
|
151
|
+
value: strInfo.value,
|
|
152
|
+
}
|
|
108
153
|
});
|
|
109
154
|
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
155
|
+
},
|
|
156
|
+
});
|
|
112
157
|
});
|
|
113
158
|
};
|
|
114
159
|
|
|
115
|
-
unvalidatedRedirect.install = function () {
|
|
116
|
-
depHooks.resolve(
|
|
117
|
-
{ name: 'fastify', file: 'lib/reply' },
|
|
118
|
-
registerUnvalidatedRedirectHandler
|
|
119
|
-
);
|
|
120
|
-
};
|
|
121
|
-
|
|
122
160
|
return unvalidatedRedirect;
|
|
123
161
|
};
|
|
@@ -0,0 +1,136 @@
|
|
|
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 { patchType } = require('../common');
|
|
18
|
+
const { FS_METHODS, Rule, isString, DataflowTag: { URL_ENCODED, LIMITED_CHARS, ALPHANUM_SPACE_HYPHEN, SAFE_PATH, UNTRUSTED } } = require('@contrast/common');
|
|
19
|
+
|
|
20
|
+
module.exports = function (core) {
|
|
21
|
+
const {
|
|
22
|
+
depHooks,
|
|
23
|
+
patcher,
|
|
24
|
+
scopes: { sources },
|
|
25
|
+
assess: {
|
|
26
|
+
dataflow: {
|
|
27
|
+
tracker,
|
|
28
|
+
sinks: { isVulnerable, reportFindings },
|
|
29
|
+
eventFactory: { createSinkEvent },
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
} = core;
|
|
33
|
+
|
|
34
|
+
const safeTags = [URL_ENCODED, LIMITED_CHARS, ALPHANUM_SPACE_HYPHEN, SAFE_PATH];
|
|
35
|
+
|
|
36
|
+
function getValues(indices, args) {
|
|
37
|
+
return indices.reduce((acc, idx) => {
|
|
38
|
+
const value = args[idx];
|
|
39
|
+
if (value && isString(value)) acc.push(value);
|
|
40
|
+
return acc;
|
|
41
|
+
}, []);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const pre = (name, indices) => (data) => {
|
|
45
|
+
const store = sources.getStore()?.assess;
|
|
46
|
+
if (!store) return;
|
|
47
|
+
|
|
48
|
+
const values = getValues(indices, data.args);
|
|
49
|
+
if (!values.length) return;
|
|
50
|
+
|
|
51
|
+
const args = [];
|
|
52
|
+
for (let i = 0; i < values.length; i++) {
|
|
53
|
+
const strInfo = tracker.getData(values[i]);
|
|
54
|
+
args.push({ value: strInfo ? strInfo.value : values[i], isTracked: !!strInfo });
|
|
55
|
+
if (!strInfo || !isVulnerable(UNTRUSTED, safeTags, strInfo.tags)) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const event = createSinkEvent({
|
|
60
|
+
name,
|
|
61
|
+
history: [strInfo],
|
|
62
|
+
object: {
|
|
63
|
+
value: 'fs',
|
|
64
|
+
isTracked: false,
|
|
65
|
+
},
|
|
66
|
+
args,
|
|
67
|
+
tags: strInfo.tags,
|
|
68
|
+
source: `P${i}`,
|
|
69
|
+
stacktraceOpts: {
|
|
70
|
+
contructorOpt: data.hooked,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (event) {
|
|
75
|
+
reportFindings({
|
|
76
|
+
ruleId: Rule.PATH_TRAVERSAL,
|
|
77
|
+
sinkEvent: event,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
core.assess.dataflow.sinks.pathTraversal = {
|
|
84
|
+
install() {
|
|
85
|
+
depHooks.resolve({ name: 'fs' }, (fs) => {
|
|
86
|
+
for (const method of FS_METHODS) {
|
|
87
|
+
// not all methods are available on every OS or Node version.
|
|
88
|
+
if (fs[method.name]) {
|
|
89
|
+
const name = `fs.${method.name}`;
|
|
90
|
+
patcher.patch(fs, method.name, {
|
|
91
|
+
name,
|
|
92
|
+
patchType,
|
|
93
|
+
pre: pre(name, method.indices)
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (method.sync) {
|
|
98
|
+
const syncName = `${method.name}Sync`;
|
|
99
|
+
if (fs[syncName]) {
|
|
100
|
+
const name = `fs.${syncName}`;
|
|
101
|
+
patcher.patch(fs, syncName, {
|
|
102
|
+
name,
|
|
103
|
+
patchType,
|
|
104
|
+
pre: pre(name, method.indices)
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (method.promises && fs.promises && fs.promises[method.name]) {
|
|
110
|
+
const name = `fs.promises.${method.name}`;
|
|
111
|
+
patcher.patch(fs.promises, method.name, {
|
|
112
|
+
name,
|
|
113
|
+
patchType,
|
|
114
|
+
pre: pre(name, method.indices)
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
depHooks.resolve({ name: 'fs/promises' }, (fsPromises) => {
|
|
121
|
+
for (const method of FS_METHODS) {
|
|
122
|
+
if (method.promises && fsPromises[method.name]) {
|
|
123
|
+
const name = `fsPromises.${method.name}`;
|
|
124
|
+
patcher.patch(fsPromises, method.name, {
|
|
125
|
+
name,
|
|
126
|
+
patchType,
|
|
127
|
+
pre: pre(name, method.indices)
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return core.assess.dataflow.sinks.pathTraversal;
|
|
136
|
+
};
|
|
@@ -15,18 +15,39 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
-
const {
|
|
19
|
-
|
|
18
|
+
const {
|
|
19
|
+
DataflowTag: {
|
|
20
|
+
UNTRUSTED,
|
|
21
|
+
ALPHANUM_SPACE_HYPHEN,
|
|
22
|
+
COOKIE,
|
|
23
|
+
CUSTOM_ENCODED,
|
|
24
|
+
CUSTOM_VALIDATED,
|
|
25
|
+
HEADER,
|
|
26
|
+
HTML_ENCODED,
|
|
27
|
+
LIMITED_CHARS,
|
|
28
|
+
SQL_ENCODED,
|
|
29
|
+
URL_ENCODED,
|
|
30
|
+
WEAK_URL_ENCODED,
|
|
31
|
+
},
|
|
32
|
+
Rule: { REFLECTED_XSS: ruleId },
|
|
33
|
+
} = require('@contrast/common');
|
|
34
|
+
const { patchType, filterSafeTags } = require('../common');
|
|
20
35
|
|
|
21
36
|
module.exports = function(core) {
|
|
22
37
|
const {
|
|
38
|
+
config,
|
|
23
39
|
depHooks,
|
|
24
40
|
patcher,
|
|
25
41
|
scopes: { sources },
|
|
26
42
|
assess: {
|
|
27
43
|
dataflow: {
|
|
28
44
|
tracker,
|
|
29
|
-
sinks: {
|
|
45
|
+
sinks: {
|
|
46
|
+
isVulnerable,
|
|
47
|
+
reportFindings,
|
|
48
|
+
reportSafePositive,
|
|
49
|
+
isSafeContentType
|
|
50
|
+
},
|
|
30
51
|
eventFactory: { createSinkEvent },
|
|
31
52
|
},
|
|
32
53
|
},
|
|
@@ -34,18 +55,17 @@ module.exports = function(core) {
|
|
|
34
55
|
const http = core.assess.dataflow.sinks.http = {};
|
|
35
56
|
|
|
36
57
|
const safeTags = [
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
58
|
+
ALPHANUM_SPACE_HYPHEN,
|
|
59
|
+
COOKIE,
|
|
60
|
+
CUSTOM_ENCODED,
|
|
61
|
+
CUSTOM_VALIDATED,
|
|
62
|
+
HEADER,
|
|
63
|
+
HTML_ENCODED,
|
|
64
|
+
LIMITED_CHARS,
|
|
65
|
+
SQL_ENCODED,
|
|
66
|
+
URL_ENCODED,
|
|
67
|
+
WEAK_URL_ENCODED,
|
|
47
68
|
];
|
|
48
|
-
const requiredTag = 'untrusted';
|
|
49
69
|
|
|
50
70
|
const preHook = (name, method) => (data) => {
|
|
51
71
|
const sourceContext = sources.getStore()?.assess;
|
|
@@ -60,7 +80,7 @@ module.exports = function(core) {
|
|
|
60
80
|
const { contentType } = sourceContext.responseData;
|
|
61
81
|
if (contentType && isSafeContentType(contentType)) return;
|
|
62
82
|
|
|
63
|
-
if (isVulnerable(
|
|
83
|
+
if (isVulnerable(UNTRUSTED, safeTags, strInfo.tags)) {
|
|
64
84
|
const event = createSinkEvent({
|
|
65
85
|
args: [{
|
|
66
86
|
value: strInfo.value,
|
|
@@ -77,7 +97,6 @@ module.exports = function(core) {
|
|
|
77
97
|
value: data.result,
|
|
78
98
|
tracked: false,
|
|
79
99
|
},
|
|
80
|
-
ruleId: Rule.REFLECTED_XSS,
|
|
81
100
|
source: 'P0',
|
|
82
101
|
stacktraceOpts: {
|
|
83
102
|
constructorOpt: data.hooked
|
|
@@ -87,10 +106,20 @@ module.exports = function(core) {
|
|
|
87
106
|
|
|
88
107
|
if (event) {
|
|
89
108
|
reportFindings({
|
|
90
|
-
ruleId
|
|
109
|
+
ruleId,
|
|
91
110
|
sinkEvent: event
|
|
92
111
|
});
|
|
93
112
|
}
|
|
113
|
+
} else if (config.assess.safe_positives.enable) {
|
|
114
|
+
reportSafePositive({
|
|
115
|
+
name,
|
|
116
|
+
ruleId,
|
|
117
|
+
safeTags: filterSafeTags(safeTags, strInfo),
|
|
118
|
+
strInfo: {
|
|
119
|
+
value: strInfo.value,
|
|
120
|
+
tags: strInfo.tags,
|
|
121
|
+
}
|
|
122
|
+
});
|
|
94
123
|
}
|
|
95
124
|
};
|
|
96
125
|
|
|
@@ -16,19 +16,32 @@
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
18
|
const util = require('util');
|
|
19
|
-
const {
|
|
20
|
-
|
|
19
|
+
const {
|
|
20
|
+
DataflowTag: {
|
|
21
|
+
UNTRUSTED,
|
|
22
|
+
CUSTOM_ENCODED,
|
|
23
|
+
CUSTOM_VALIDATED,
|
|
24
|
+
HTML_ENCODED,
|
|
25
|
+
LIMITED_CHARS,
|
|
26
|
+
URL_ENCODED,
|
|
27
|
+
},
|
|
28
|
+
isString
|
|
29
|
+
} = require('@contrast/common');
|
|
21
30
|
const { createSubsetTags } = require('../../../tag-utils');
|
|
31
|
+
const { filterSafeTags, patchType } = require('../../common');
|
|
32
|
+
|
|
33
|
+
const ruleId = 'unvalidated-redirect';
|
|
22
34
|
|
|
23
35
|
module.exports = function (core) {
|
|
24
36
|
const {
|
|
25
37
|
depHooks,
|
|
26
38
|
patcher,
|
|
39
|
+
config,
|
|
27
40
|
scopes: { sources },
|
|
28
41
|
assess: {
|
|
29
42
|
dataflow: {
|
|
30
43
|
tracker,
|
|
31
|
-
sinks: { isVulnerable, reportFindings },
|
|
44
|
+
sinks: { isVulnerable, reportFindings, reportSafePositive },
|
|
32
45
|
eventFactory: { createSinkEvent },
|
|
33
46
|
},
|
|
34
47
|
},
|
|
@@ -39,16 +52,16 @@ module.exports = function (core) {
|
|
|
39
52
|
const inspect = patcher.unwrap(util.inspect);
|
|
40
53
|
|
|
41
54
|
const safeTags = [
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
55
|
+
CUSTOM_ENCODED,
|
|
56
|
+
CUSTOM_VALIDATED,
|
|
57
|
+
HTML_ENCODED,
|
|
58
|
+
LIMITED_CHARS,
|
|
59
|
+
URL_ENCODED,
|
|
47
60
|
];
|
|
48
|
-
const requiredTag = 'untrusted';
|
|
49
61
|
|
|
50
62
|
unvalidatedRedirect.install = function () {
|
|
51
63
|
depHooks.resolve({ name: 'koa', file: 'lib/response', version: '<2.9.0' }, (Response) => {
|
|
64
|
+
const name = 'Koa.Response.redirect';
|
|
52
65
|
patcher.patch(Response, 'redirect', {
|
|
53
66
|
name: 'Koa.Response.redirect',
|
|
54
67
|
patchType,
|
|
@@ -56,9 +69,10 @@ module.exports = function (core) {
|
|
|
56
69
|
const assessStore = sources.getStore()?.assess;
|
|
57
70
|
if (!assessStore) return;
|
|
58
71
|
|
|
72
|
+
let isBackRoute = false;
|
|
59
73
|
let [url] = data.args;
|
|
60
|
-
|
|
61
74
|
if (url === 'back') {
|
|
75
|
+
isBackRoute = true;
|
|
62
76
|
url = data.obj.ctx.get('Referrer') || data.args[1];
|
|
63
77
|
}
|
|
64
78
|
|
|
@@ -73,12 +87,21 @@ module.exports = function (core) {
|
|
|
73
87
|
urlPathTags = createSubsetTags(strInfo.tags, 0, url.indexOf('?'));
|
|
74
88
|
}
|
|
75
89
|
|
|
76
|
-
if (urlPathTags && isVulnerable(
|
|
90
|
+
if (urlPathTags && isVulnerable(UNTRUSTED, safeTags, urlPathTags)) {
|
|
91
|
+
const args = [];
|
|
92
|
+
if (isBackRoute) {
|
|
93
|
+
args.push({
|
|
94
|
+
tracked: false,
|
|
95
|
+
value: data.args[0]
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
args.push({
|
|
99
|
+
tracked: true,
|
|
100
|
+
value: strInfo.value,
|
|
101
|
+
});
|
|
102
|
+
|
|
77
103
|
const event = createSinkEvent({
|
|
78
|
-
args
|
|
79
|
-
tracked: true,
|
|
80
|
-
value: strInfo.value,
|
|
81
|
-
}],
|
|
104
|
+
args,
|
|
82
105
|
context: `response.redirect(${inspect(strInfo.value)})`,
|
|
83
106
|
history: [strInfo],
|
|
84
107
|
name: 'Koa.Response.redirect',
|
|
@@ -91,7 +114,7 @@ module.exports = function (core) {
|
|
|
91
114
|
value: undefined,
|
|
92
115
|
},
|
|
93
116
|
tags: urlPathTags,
|
|
94
|
-
source:
|
|
117
|
+
source: `P${isBackRoute ? 1 : 0}`,
|
|
95
118
|
stacktraceOpts: {
|
|
96
119
|
constructorOpt: data.hooked,
|
|
97
120
|
},
|
|
@@ -99,10 +122,20 @@ module.exports = function (core) {
|
|
|
99
122
|
|
|
100
123
|
if (event) {
|
|
101
124
|
reportFindings({
|
|
102
|
-
ruleId
|
|
125
|
+
ruleId,
|
|
103
126
|
sinkEvent: event,
|
|
104
127
|
});
|
|
105
128
|
}
|
|
129
|
+
} else if (config.assess.safe_positives.enable) {
|
|
130
|
+
reportSafePositive({
|
|
131
|
+
name,
|
|
132
|
+
ruleId,
|
|
133
|
+
safeTags: filterSafeTags(safeTags, strInfo),
|
|
134
|
+
strInfo: {
|
|
135
|
+
tags: strInfo.tags,
|
|
136
|
+
value: strInfo.value,
|
|
137
|
+
}
|
|
138
|
+
});
|
|
106
139
|
}
|
|
107
140
|
}
|
|
108
141
|
});
|