@contrast/assess 1.12.0 → 1.14.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/LICENSE +12 -0
- package/lib/dataflow/propagation/index.js +2 -0
- package/lib/dataflow/propagation/install/buffer.js +6 -5
- package/lib/dataflow/propagation/install/contrast-methods/add.js +3 -0
- package/lib/dataflow/propagation/install/joi/any.js +46 -0
- package/lib/dataflow/propagation/install/joi/boolean.js +109 -0
- package/lib/dataflow/propagation/install/joi/expression.js +99 -0
- package/lib/dataflow/propagation/install/joi/index.js +172 -0
- package/lib/dataflow/propagation/install/joi/keys.js +140 -0
- package/lib/dataflow/propagation/install/joi/number.js +107 -0
- package/lib/dataflow/propagation/install/joi/object.js +46 -0
- package/lib/dataflow/propagation/install/joi/string-schema.js +233 -0
- package/lib/dataflow/propagation/install/joi/utils.js +111 -0
- package/lib/dataflow/propagation/install/joi/values.js +154 -0
- package/lib/dataflow/propagation/install/path/basename.js +1 -3
- package/lib/dataflow/propagation/install/path/join-and-resolve.js +1 -3
- package/lib/dataflow/propagation/install/path/normalize.js +1 -3
- package/lib/dataflow/propagation/install/pug/index.js +2 -2
- package/lib/dataflow/propagation/install/reg-exp-prototype-exec.js +7 -6
- package/lib/dataflow/propagation/install/send.js +60 -0
- package/lib/dataflow/propagation/install/sequelize.js +4 -4
- package/lib/dataflow/propagation/install/string/match-all.js +6 -5
- package/lib/dataflow/propagation/install/string/match.js +14 -11
- package/lib/dataflow/propagation/install/string/replace.js +6 -5
- package/lib/dataflow/propagation/install/string/slice.js +3 -0
- package/lib/dataflow/propagation/install/string/split.js +6 -5
- package/lib/dataflow/propagation/install/string/substring.js +1 -3
- package/lib/dataflow/propagation/install/string/trim.js +2 -0
- package/lib/dataflow/propagation/install/url/parse.js +3 -8
- package/lib/dataflow/propagation/install/url/searchParams.js +7 -7
- package/lib/dataflow/propagation/install/validator/hooks.js +4 -4
- package/lib/dataflow/sinks/index.js +3 -3
- package/lib/dataflow/sinks/install/eval.js +63 -67
- package/lib/dataflow/sinks/install/fs.js +2 -2
- package/lib/dataflow/sinks/install/function.js +87 -91
- package/lib/dataflow/sinks/install/vm.js +7 -7
- package/lib/dataflow/sources/install/body-parser1.js +2 -2
- package/lib/dataflow/sources/install/fastify/fastify.js +1 -1
- package/lib/dataflow/sources/install/http.js +11 -10
- package/lib/dataflow/tracker.js +1 -3
- package/lib/response-scanning/install/http.js +3 -2
- package/package.json +14 -11
|
@@ -45,7 +45,7 @@ module.exports = function(core) {
|
|
|
45
45
|
context: `'${obj}'.exec('${strInfo.value}')`,
|
|
46
46
|
history: [strInfo],
|
|
47
47
|
object: {
|
|
48
|
-
value:
|
|
48
|
+
value: 'RegExp',
|
|
49
49
|
tracked: false,
|
|
50
50
|
},
|
|
51
51
|
args: [
|
|
@@ -162,11 +162,12 @@ module.exports = function(core) {
|
|
|
162
162
|
metadata
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
-
if (event)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
165
|
+
if (!event) return;
|
|
166
|
+
|
|
167
|
+
const { extern } = tracker.track(res, event);
|
|
168
|
+
|
|
169
|
+
if (extern) {
|
|
170
|
+
result.groups[key] = extern;
|
|
170
171
|
}
|
|
171
172
|
});
|
|
172
173
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright: 2023 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
|
+
|
|
17
|
+
const { patchType } = require('../common');
|
|
18
|
+
const { slice } = require('@contrast/common');
|
|
19
|
+
|
|
20
|
+
module.exports = function (core) {
|
|
21
|
+
const {
|
|
22
|
+
scopes: { sources, instrumentation },
|
|
23
|
+
depHooks,
|
|
24
|
+
patcher
|
|
25
|
+
} = core;
|
|
26
|
+
|
|
27
|
+
const send = {};
|
|
28
|
+
core.assess.dataflow.propagation.send = send;
|
|
29
|
+
|
|
30
|
+
function patchSendModule(sendModuleExport) {
|
|
31
|
+
return patcher.patch(sendModuleExport, {
|
|
32
|
+
name: 'send',
|
|
33
|
+
patchType,
|
|
34
|
+
post(data) {
|
|
35
|
+
patcher.patch(data.result, 'sendFile', {
|
|
36
|
+
name: 'send.sendFile',
|
|
37
|
+
patchType,
|
|
38
|
+
pre(data) {
|
|
39
|
+
const { args } = data;
|
|
40
|
+
|
|
41
|
+
if (!sources.getStore()?.assess || instrumentation.isLocked()) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const untrackedPath = slice(` ${args[0]}`, 1);
|
|
46
|
+
args[0] = untrackedPath;
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
send.install = function () {
|
|
54
|
+
depHooks.resolve({ name: 'send' }, (sendModule) =>
|
|
55
|
+
patchSendModule(sendModule)
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return send;
|
|
60
|
+
};
|
|
@@ -32,7 +32,7 @@ module.exports = function(core) {
|
|
|
32
32
|
},
|
|
33
33
|
} = core;
|
|
34
34
|
|
|
35
|
-
function
|
|
35
|
+
function getFormatPositions(str) {
|
|
36
36
|
const positions = [];
|
|
37
37
|
let index = -1;
|
|
38
38
|
|
|
@@ -50,7 +50,7 @@ module.exports = function(core) {
|
|
|
50
50
|
return Array.from(matches, (match) => ({ [match[1]]: match.index }));
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
return
|
|
53
|
+
return core.assess.dataflow.propagation.sequelizeInstrumentation = {
|
|
54
54
|
install() {
|
|
55
55
|
depHooks.resolve(
|
|
56
56
|
{ name: 'sequelize', file: 'lib/sql-string.js' },
|
|
@@ -133,7 +133,7 @@ module.exports = function(core) {
|
|
|
133
133
|
return;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
const positions =
|
|
136
|
+
const positions = getFormatPositions(data.args[0]);
|
|
137
137
|
const firstArgInfo = tracker.getData(data.args[0]);
|
|
138
138
|
|
|
139
139
|
if (!positions.length) {
|
|
@@ -307,5 +307,5 @@ module.exports = function(core) {
|
|
|
307
307
|
}
|
|
308
308
|
);
|
|
309
309
|
},
|
|
310
|
-
}
|
|
310
|
+
};
|
|
311
311
|
};
|
|
@@ -195,11 +195,12 @@ module.exports = function(core) {
|
|
|
195
195
|
});
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
-
if (event)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
198
|
+
if (!event) return;
|
|
199
|
+
|
|
200
|
+
const { extern } = tracker.track(res, event);
|
|
201
|
+
|
|
202
|
+
if (extern) {
|
|
203
|
+
resValue.groups[key] = extern;
|
|
203
204
|
}
|
|
204
205
|
});
|
|
205
206
|
}
|
|
@@ -30,6 +30,7 @@ module.exports = function(core) {
|
|
|
30
30
|
},
|
|
31
31
|
},
|
|
32
32
|
} = core;
|
|
33
|
+
|
|
33
34
|
const name = 'String.prototype.match';
|
|
34
35
|
|
|
35
36
|
function getPropagationEvent(data, res, objInfo, start) {
|
|
@@ -49,7 +50,7 @@ module.exports = function(core) {
|
|
|
49
50
|
moduleName: 'String',
|
|
50
51
|
methodName: 'prototype.match',
|
|
51
52
|
context: `'${objInfo.value}'.match(${args[0].value})`,
|
|
52
|
-
history: [objInfo],
|
|
53
|
+
history: [{ ...objInfo }],
|
|
53
54
|
object: {
|
|
54
55
|
value: objInfo.value,
|
|
55
56
|
tracked: true,
|
|
@@ -131,11 +132,12 @@ module.exports = function(core) {
|
|
|
131
132
|
event = getPropagationEvent(data, res, objInfo, start);
|
|
132
133
|
}
|
|
133
134
|
|
|
134
|
-
if (event)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
135
|
+
if (!event) continue;
|
|
136
|
+
|
|
137
|
+
const { extern } = tracker.track(res, event);
|
|
138
|
+
|
|
139
|
+
if (extern) {
|
|
140
|
+
data.result[i] = extern;
|
|
139
141
|
}
|
|
140
142
|
}
|
|
141
143
|
if (hasCaptureGroups && result.groups) {
|
|
@@ -153,11 +155,12 @@ module.exports = function(core) {
|
|
|
153
155
|
event = getPropagationEvent(data, res, objInfo, start);
|
|
154
156
|
}
|
|
155
157
|
|
|
156
|
-
if (event)
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
if (!event) return;
|
|
159
|
+
|
|
160
|
+
const { extern } = tracker.track(res, event);
|
|
161
|
+
|
|
162
|
+
if (extern) {
|
|
163
|
+
data.result.groups[key] = extern;
|
|
161
164
|
}
|
|
162
165
|
});
|
|
163
166
|
}
|
|
@@ -199,11 +199,12 @@ module.exports = function(core) {
|
|
|
199
199
|
target: 'R',
|
|
200
200
|
});
|
|
201
201
|
|
|
202
|
-
if (event)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
202
|
+
if (!event) return;
|
|
203
|
+
|
|
204
|
+
const { extern } = tracker.track(result, event);
|
|
205
|
+
|
|
206
|
+
if (extern) {
|
|
207
|
+
data.result = extern;
|
|
207
208
|
}
|
|
208
209
|
}
|
|
209
210
|
});
|
|
@@ -95,11 +95,12 @@ module.exports = function(core) {
|
|
|
95
95
|
target: 'R'
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
-
if (event)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
if (!event) continue;
|
|
99
|
+
|
|
100
|
+
const { extern } = tracker.track(res, event);
|
|
101
|
+
|
|
102
|
+
if (extern) {
|
|
103
|
+
data.result[i] = extern;
|
|
103
104
|
}
|
|
104
105
|
}
|
|
105
106
|
}
|
|
@@ -106,16 +106,11 @@ module.exports = function(core) {
|
|
|
106
106
|
prependFrames: [orig]
|
|
107
107
|
},
|
|
108
108
|
});
|
|
109
|
-
if (!event) return;
|
|
110
109
|
|
|
111
|
-
if (
|
|
112
|
-
Object.assign(partInfo, event);
|
|
113
|
-
}
|
|
114
|
-
const { extern } = partInfo || tracker.track(part, event);
|
|
110
|
+
if (!event) return;
|
|
115
111
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
112
|
+
Object.assign(partInfo, event);
|
|
113
|
+
result[key] = substr;
|
|
119
114
|
}
|
|
120
115
|
} else {
|
|
121
116
|
traverse(substr, url, key, 0);
|
|
@@ -90,10 +90,8 @@ module.exports = function(core) {
|
|
|
90
90
|
if (event) Object.assign(paramInfo, event);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (trackedKey) result.delete(key);
|
|
96
|
-
result.set(trackedKey || key, trackedParam || param);
|
|
93
|
+
if (keyInfo) result.delete(key);
|
|
94
|
+
result.set(key, param);
|
|
97
95
|
});
|
|
98
96
|
}
|
|
99
97
|
|
|
@@ -106,11 +104,13 @@ module.exports = function(core) {
|
|
|
106
104
|
if (!event) return;
|
|
107
105
|
|
|
108
106
|
Object.assign(paramInfo, event);
|
|
109
|
-
|
|
107
|
+
let value = params[key];
|
|
110
108
|
|
|
111
|
-
if (
|
|
112
|
-
|
|
109
|
+
if (!paramInfo) {
|
|
110
|
+
({ extern: value } = tracker.track(params[key], event));
|
|
113
111
|
}
|
|
112
|
+
|
|
113
|
+
result.set(key, value);
|
|
114
114
|
});
|
|
115
115
|
}
|
|
116
116
|
|
|
@@ -132,13 +132,13 @@ module.exports = function(core) {
|
|
|
132
132
|
'R'
|
|
133
133
|
);
|
|
134
134
|
let resultTracked = tracker.getData(data.result);
|
|
135
|
-
|
|
136
|
-
resultTracked = tracker.track(data.result, event);
|
|
137
|
-
if (resultTracked.extern) data.result = resultTracked.extern;
|
|
138
|
-
}
|
|
135
|
+
|
|
139
136
|
if (event) {
|
|
137
|
+
resultTracked = resultTracked || tracker.track(data.result, event);
|
|
140
138
|
Object.assign(resultTracked, event);
|
|
141
139
|
}
|
|
140
|
+
|
|
141
|
+
if (resultTracked.extern) data.result = resultTracked.extern;
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
}));
|
|
@@ -20,7 +20,7 @@ const { callChildComponentMethodsSync, Event, Rule } = require('@contrast/common
|
|
|
20
20
|
const { isVulnerable } = require('../utils/is-vulnerable');
|
|
21
21
|
const { isSafeContentType } = require('../utils/is-safe-content-type');
|
|
22
22
|
|
|
23
|
-
module.exports = function(core) {
|
|
23
|
+
module.exports = function (core) {
|
|
24
24
|
const {
|
|
25
25
|
logger,
|
|
26
26
|
messages,
|
|
@@ -30,7 +30,7 @@ module.exports = function(core) {
|
|
|
30
30
|
const sinkScopes = {
|
|
31
31
|
[Rule.SQL_INJECTION]: new AsyncLocalStorage(),
|
|
32
32
|
[Rule.NOSQL_INJECTION_MONGO]: new AsyncLocalStorage(),
|
|
33
|
-
[
|
|
33
|
+
[Rule.UNSAFE_CODE_EXECUTION]: new AsyncLocalStorage()
|
|
34
34
|
};
|
|
35
35
|
const sinks = core.assess.dataflow.sinks = {
|
|
36
36
|
isVulnerable,
|
|
@@ -83,7 +83,7 @@ module.exports = function(core) {
|
|
|
83
83
|
require('./install/sqlite3')(core);
|
|
84
84
|
require('./install/vm')(core);
|
|
85
85
|
|
|
86
|
-
sinks.install = function() {
|
|
86
|
+
sinks.install = function () {
|
|
87
87
|
callChildComponentMethodsSync(core.assess.dataflow.sinks, 'install');
|
|
88
88
|
};
|
|
89
89
|
|
|
@@ -25,10 +25,10 @@ const {
|
|
|
25
25
|
CUSTOM_VALIDATED,
|
|
26
26
|
LIMITED_CHARS,
|
|
27
27
|
},
|
|
28
|
+
Rule: { UNSAFE_CODE_EXECUTION },
|
|
28
29
|
} = require('@contrast/common');
|
|
29
30
|
const { patchType, filterSafeTags } = require('../common');
|
|
30
31
|
|
|
31
|
-
const ruleId = 'unsafe-code-execution';
|
|
32
32
|
const safeTags = [
|
|
33
33
|
CUSTOM_ENCODED_TRUST_BOUNDARY_VIOLATION,
|
|
34
34
|
CUSTOM_ENCODED,
|
|
@@ -37,7 +37,7 @@ const safeTags = [
|
|
|
37
37
|
LIMITED_CHARS,
|
|
38
38
|
];
|
|
39
39
|
|
|
40
|
-
module.exports = function(core) {
|
|
40
|
+
module.exports = function (core) {
|
|
41
41
|
const {
|
|
42
42
|
config,
|
|
43
43
|
logger,
|
|
@@ -55,83 +55,79 @@ module.exports = function(core) {
|
|
|
55
55
|
core.assess.dataflow.sinks.contrastEval = {
|
|
56
56
|
install() {
|
|
57
57
|
if (!global.ContrastMethods?.eval) {
|
|
58
|
-
logger.error(
|
|
59
|
-
'Cannot install `eval` instrumentation - Contrast method DNE'
|
|
60
|
-
);
|
|
58
|
+
logger.error('Cannot install `eval` instrumentation - Contrast method DNE');
|
|
61
59
|
return;
|
|
62
60
|
}
|
|
63
61
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
62
|
+
patcher.patch(global.ContrastMethods, 'eval', {
|
|
63
|
+
name: 'global.ContrastMethods.eval',
|
|
64
|
+
patchType,
|
|
65
|
+
pre({ args, orig }) {
|
|
66
|
+
const store = sources.getStore()?.assess;
|
|
67
|
+
const script = args[0];
|
|
68
|
+
if (
|
|
69
|
+
!store ||
|
|
70
|
+
instrumentation.isLocked() ||
|
|
71
|
+
!script ||
|
|
72
|
+
!isString(script)
|
|
73
|
+
) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
78
76
|
|
|
79
|
-
|
|
77
|
+
const strInfo = tracker.getData(script);
|
|
80
78
|
|
|
81
|
-
|
|
79
|
+
if (!strInfo) return;
|
|
82
80
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
81
|
+
const isArgVulnerable = isVulnerable(
|
|
82
|
+
UNTRUSTED,
|
|
83
|
+
safeTags,
|
|
84
|
+
strInfo.tags
|
|
85
|
+
);
|
|
88
86
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
87
|
+
if (!isArgVulnerable && config.assess.safe_positives.enable) {
|
|
88
|
+
const foundSafeTags = filterSafeTags(safeTags, strInfo);
|
|
89
|
+
const safeStrInfo = {
|
|
90
|
+
value: strInfo.value,
|
|
91
|
+
tags: strInfo.tags,
|
|
92
|
+
};
|
|
95
93
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
94
|
+
reportSafePositive({
|
|
95
|
+
name: 'eval',
|
|
96
|
+
ruleId: UNSAFE_CODE_EXECUTION,
|
|
97
|
+
safeTags: foundSafeTags,
|
|
98
|
+
strInfo: safeStrInfo,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
103
101
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
});
|
|
102
|
+
if (isArgVulnerable) {
|
|
103
|
+
const event = createSinkEvent({
|
|
104
|
+
name: 'eval',
|
|
105
|
+
context: `eval('${strInfo.value}')`,
|
|
106
|
+
history: [strInfo],
|
|
107
|
+
object: {
|
|
108
|
+
value: 'global',
|
|
109
|
+
tracked: false,
|
|
110
|
+
},
|
|
111
|
+
moduleName: 'global',
|
|
112
|
+
methodName: 'eval',
|
|
113
|
+
args: [{ value: strInfo.value, tracked: true }],
|
|
114
|
+
tags: strInfo.tags,
|
|
115
|
+
source: 'P0',
|
|
116
|
+
stacktraceOpts: {
|
|
117
|
+
contructorOpt: orig
|
|
118
|
+
},
|
|
119
|
+
});
|
|
123
120
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
121
|
+
if (event) {
|
|
122
|
+
reportFindings({
|
|
123
|
+
ruleId: UNSAFE_CODE_EXECUTION,
|
|
124
|
+
sinkEvent: event,
|
|
125
|
+
});
|
|
130
126
|
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
127
|
+
}
|
|
128
|
+
},
|
|
133
129
|
});
|
|
134
|
-
}
|
|
130
|
+
}
|
|
135
131
|
};
|
|
136
132
|
|
|
137
133
|
return core.assess.dataflow.sinks.contrastEval;
|
|
@@ -34,7 +34,7 @@ module.exports = function(core) {
|
|
|
34
34
|
const {
|
|
35
35
|
depHooks,
|
|
36
36
|
patcher,
|
|
37
|
-
scopes: { sources },
|
|
37
|
+
scopes: { instrumentation, sources },
|
|
38
38
|
assess: {
|
|
39
39
|
eventFactory: { createSinkEvent },
|
|
40
40
|
dataflow: {
|
|
@@ -62,7 +62,7 @@ module.exports = function(core) {
|
|
|
62
62
|
const pre = (name, method, moduleName = 'fs', fullMethodName = '') => (data) => {
|
|
63
63
|
const { name: methodName, indices } = method;
|
|
64
64
|
const store = sources.getStore()?.assess;
|
|
65
|
-
if (!store) return;
|
|
65
|
+
if (!store || instrumentation.isLocked()) return;
|
|
66
66
|
|
|
67
67
|
const values = getValues(indices, data.args);
|
|
68
68
|
if (!values.length) return;
|