@contrast/assess 1.77.0 → 1.79.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 +2 -0
- package/lib/dataflow/propagation/install/JSON/stringify.js +14 -141
- package/lib/dataflow/propagation/install/JSON/tag-ranges.js +174 -0
- package/lib/dataflow/propagation/install/jsonwebtoken.js +1 -1
- package/lib/dataflow/propagation/install/sanitize-html.js +1 -1
- package/lib/dataflow/propagation/install/string/concat.js +23 -2
- package/lib/dataflow/propagation/install/url/url.js +56 -22
- package/lib/dataflow/propagation/install/util-format.js +43 -3
- package/lib/dataflow/propagation/install/zod/matrix.js +149 -0
- package/lib/dataflow/propagation/install/zod/process-result-v3.js +228 -0
- package/lib/dataflow/propagation/install/zod/process-result-v4.js +191 -0
- package/lib/dataflow/propagation/install/zod/utils.js +227 -0
- package/lib/dataflow/propagation/install/zod/v3.js +117 -0
- package/lib/dataflow/propagation/install/zod/v4.js +97 -0
- package/lib/dataflow/sinks/install/child-process.js +1 -1
- package/lib/dataflow/sinks/install/eval.js +1 -1
- package/lib/dataflow/sinks/install/fs.js +1 -1
- package/lib/dataflow/sinks/install/function.js +1 -1
- package/lib/dataflow/sinks/install/libxmljs.js +1 -1
- package/lib/dataflow/sinks/install/mssql.js +1 -1
- package/lib/dataflow/sinks/install/mysql.js +1 -1
- package/lib/dataflow/sinks/install/node-serialize.js +1 -1
- package/lib/dataflow/sinks/install/sequelize.js +1 -1
- package/lib/dataflow/sinks/install/sqlite3.js +1 -1
- package/lib/dataflow/sinks/install/vm.js +1 -1
- package/lib/dataflow/sources/handler.js +1 -1
- package/lib/dataflow/sources/install/@sap.js +14 -2
- package/lib/dataflow/sources/install/fastify-websocket.js +1 -1
- package/lib/dataflow/sources/install/socket.io.js +1 -1
- package/lib/event-factory.js +1 -1
- package/lib/get-source-context.js +1 -1
- package/lib/index.js +1 -1
- package/lib/make-source-context.js +1 -1
- package/lib/policy.js +1 -1
- package/package.json +16 -16
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright: 2026 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
|
+
DataflowTag: { CUSTOM_VALIDATED },
|
|
20
|
+
isNonEmptyObject,
|
|
21
|
+
traverseValues,
|
|
22
|
+
} = require('@contrast/common');
|
|
23
|
+
const { createFullLengthCopyTags } = require('../../../tag-utils');
|
|
24
|
+
|
|
25
|
+
module.exports = function(core) {
|
|
26
|
+
const {
|
|
27
|
+
assess: {
|
|
28
|
+
getPropagatorContext,
|
|
29
|
+
inspect,
|
|
30
|
+
eventFactory: { createPropagationEvent },
|
|
31
|
+
dataflow: { tracker },
|
|
32
|
+
},
|
|
33
|
+
config,
|
|
34
|
+
} = core;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Merges the given tags onto an already-tracked string's existing tracking
|
|
38
|
+
* info, in place. Used for checks that validate a string without changing
|
|
39
|
+
* its identity (format checks, length checks, refine/superRefine).
|
|
40
|
+
*/
|
|
41
|
+
function applyTags(value, tags, schema, methodName) {
|
|
42
|
+
if (!tags.size || typeof value !== 'string') return;
|
|
43
|
+
|
|
44
|
+
const strInfo = tracker.getData(value);
|
|
45
|
+
if (!strInfo) return;
|
|
46
|
+
|
|
47
|
+
const event = createPropagationEvent({
|
|
48
|
+
name: `zod.${methodName}`,
|
|
49
|
+
moduleName: 'zod',
|
|
50
|
+
methodName,
|
|
51
|
+
history: [{ ...strInfo }],
|
|
52
|
+
object: { tracked: false, value: 'zod.schema' },
|
|
53
|
+
args: [
|
|
54
|
+
{ tracked: true, value: strInfo.value },
|
|
55
|
+
{ tracked: false, value: inspect(schema) },
|
|
56
|
+
],
|
|
57
|
+
result: { tracked: false, value: undefined },
|
|
58
|
+
source: 'P0',
|
|
59
|
+
tags: Object.assign(
|
|
60
|
+
{ ...strInfo.tags },
|
|
61
|
+
...[...tags].map((tag) => ({ [tag]: [0, strInfo.value.length - 1] }))
|
|
62
|
+
),
|
|
63
|
+
target: 'P0',
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (event) Object.assign(strInfo, event);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Untracks a value that has become a member of a fixed allow-list
|
|
71
|
+
* (enum/literal) and is no longer meaningfully "tainted" freeform input.
|
|
72
|
+
*/
|
|
73
|
+
function untrackValue(value) {
|
|
74
|
+
if (typeof value !== 'string' && typeof value !== 'object') return;
|
|
75
|
+
if (!tracker.getData(value)) return;
|
|
76
|
+
tracker.untrack(value);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Generic retrack for any '.check === overwrite' check (trim/toLowerCase/
|
|
81
|
+
* toUpperCase/normalize/slugify) - the check mutates the value in place
|
|
82
|
+
* inside zod, producing a new string object. Carries the input's existing
|
|
83
|
+
* tags forward onto the new string, generalizing joi's isoDate-coercion
|
|
84
|
+
* retrack (string-schema.js's reTrackCoercedValue) to any overwrite check.
|
|
85
|
+
* Returns the (possibly externalized) result string to use going forward.
|
|
86
|
+
*/
|
|
87
|
+
function retrackOverwrite(input, result, schema) {
|
|
88
|
+
if (typeof input !== 'string' || typeof result !== 'string' || input === result) return result;
|
|
89
|
+
|
|
90
|
+
const inputInfo = tracker.getData(input);
|
|
91
|
+
if (!inputInfo) return result;
|
|
92
|
+
|
|
93
|
+
const tags = createFullLengthCopyTags(inputInfo.tags, result.length);
|
|
94
|
+
if (!tags) return result;
|
|
95
|
+
|
|
96
|
+
const event = createPropagationEvent({
|
|
97
|
+
name: 'zod.string.overwrite',
|
|
98
|
+
moduleName: 'zod',
|
|
99
|
+
methodName: 'string.overwrite',
|
|
100
|
+
history: [{ ...inputInfo }],
|
|
101
|
+
object: { tracked: false, value: 'zod.string' },
|
|
102
|
+
args: [
|
|
103
|
+
{ tracked: true, value: inputInfo.value },
|
|
104
|
+
{ tracked: false, value: inspect(schema) },
|
|
105
|
+
],
|
|
106
|
+
result: { tracked: false, value: result },
|
|
107
|
+
source: 'P0',
|
|
108
|
+
tags,
|
|
109
|
+
target: 'R',
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (!event) return result;
|
|
113
|
+
|
|
114
|
+
const { extern } = tracker.track(result, event);
|
|
115
|
+
return extern || result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Tags data derived from arbitrary user code (refine/superRefine/transform)
|
|
120
|
+
* as CUSTOM_VALIDATED, gated behind the same operator opt-in flag used by
|
|
121
|
+
* every other instrumented validator library in this codebase (joi,
|
|
122
|
+
* validator, mongoose) - trusting an arbitrary function to have performed
|
|
123
|
+
* meaningful validation/sanitization is exactly the judgment call this flag
|
|
124
|
+
* exists for.
|
|
125
|
+
*
|
|
126
|
+
* `retrack: true` (transform - the value's identity changes) tracks a fresh
|
|
127
|
+
* value derived from `sourceValue`'s tags. `retrack: false` (refine/
|
|
128
|
+
* superRefine - identity unchanged) merges the tag onto the existing value
|
|
129
|
+
* in place.
|
|
130
|
+
*/
|
|
131
|
+
function tagCustomValidated({ sourceValue, targetValue, schema, methodName, retrack }) {
|
|
132
|
+
if (!config.assess.trust_custom_validators) return targetValue;
|
|
133
|
+
|
|
134
|
+
const sourceInfo = tracker.getData(sourceValue);
|
|
135
|
+
if (!sourceInfo) return targetValue;
|
|
136
|
+
|
|
137
|
+
if (!retrack) {
|
|
138
|
+
applyTags(targetValue, new Set([CUSTOM_VALIDATED]), schema, methodName);
|
|
139
|
+
return targetValue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (typeof targetValue !== 'string') return targetValue;
|
|
143
|
+
|
|
144
|
+
const event = createPropagationEvent({
|
|
145
|
+
addedTags: [CUSTOM_VALIDATED],
|
|
146
|
+
name: `zod.${methodName}`,
|
|
147
|
+
moduleName: 'zod',
|
|
148
|
+
methodName,
|
|
149
|
+
history: [{ ...sourceInfo }],
|
|
150
|
+
object: { tracked: false, value: `zod.${methodName}` },
|
|
151
|
+
args: [
|
|
152
|
+
{ tracked: true, value: sourceInfo.value },
|
|
153
|
+
{ tracked: false, value: inspect(schema) },
|
|
154
|
+
],
|
|
155
|
+
result: { tracked: true, value: targetValue },
|
|
156
|
+
source: 'P0',
|
|
157
|
+
tags: { [CUSTOM_VALIDATED]: [0, targetValue.length - 1] },
|
|
158
|
+
target: 'R',
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (!event) return targetValue;
|
|
162
|
+
|
|
163
|
+
const { extern } = tracker.track(targetValue, event);
|
|
164
|
+
return extern || targetValue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Handles a transform-shaped boundary - v4's `.transform()`/`.pipe()`
|
|
169
|
+
* (def.type === 'pipe') and v3's `.transform()` (ZodEffects, effect.type
|
|
170
|
+
* 'transform') and explicit `.pipe()` (ZodPipeline) all reduce to the same
|
|
171
|
+
* shape once each version's process-result module has already resolved
|
|
172
|
+
* `sourceSchema` (v4's `def.in`, v3's `def.schema`/`def.in`) - see each
|
|
173
|
+
* version's own walker for that resolution. Per design, format/type tags
|
|
174
|
+
* from the source schema's checks are deliberately NOT carried forward
|
|
175
|
+
* across the boundary - an arbitrary transform function can invalidate the
|
|
176
|
+
* security guarantee those tags imply (e.g. LIMITED_CHARS after `.email()`
|
|
177
|
+
* no longer holds once a transform can do anything to the string). Only
|
|
178
|
+
* CUSTOM_VALIDATED is applied to the output, gated by
|
|
179
|
+
* trust_custom_validators, treating the output like any other
|
|
180
|
+
* arbitrary-user-code result (refine/superRefine).
|
|
181
|
+
*/
|
|
182
|
+
function handleTransformLike(sourceSchema, input, result) {
|
|
183
|
+
if (typeof result !== 'string' && !isNonEmptyObject(result)) return result;
|
|
184
|
+
if (typeof input !== 'string' && !isNonEmptyObject(input)) return result;
|
|
185
|
+
|
|
186
|
+
return tagCustomValidated({
|
|
187
|
+
sourceValue: input,
|
|
188
|
+
targetValue: result,
|
|
189
|
+
schema: sourceSchema,
|
|
190
|
+
methodName: 'transform',
|
|
191
|
+
retrack: input !== result,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* True if `value` is, or contains, any currently-tracked string. Checked
|
|
197
|
+
* against BOTH the pre-parse input and the post-parse result before
|
|
198
|
+
* deciding whether the (potentially expensive) schema walk is worth
|
|
199
|
+
* running: a retracked value (trim/toLowerCase/transform) appears
|
|
200
|
+
* untracked in `result` alone, since it's a brand new string object, so
|
|
201
|
+
* checking `result` by itself would miss every retracking case.
|
|
202
|
+
*/
|
|
203
|
+
function hasAnyTrackedValue(value) {
|
|
204
|
+
if (typeof value === 'string') return !!tracker.getData(value);
|
|
205
|
+
if (!isNonEmptyObject(value)) return false;
|
|
206
|
+
|
|
207
|
+
let found = false;
|
|
208
|
+
traverseValues(value, (_path, _type, v) => {
|
|
209
|
+
if (tracker.getData(v)) {
|
|
210
|
+
found = true;
|
|
211
|
+
return 1;
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
return found;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
applyTags,
|
|
219
|
+
untrackValue,
|
|
220
|
+
retrackOverwrite,
|
|
221
|
+
tagCustomValidated,
|
|
222
|
+
handleTransformLike,
|
|
223
|
+
hasAnyTrackedValue,
|
|
224
|
+
getPropagatorContext,
|
|
225
|
+
tracker,
|
|
226
|
+
};
|
|
227
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright: 2026 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 { patchType } = require('../../common');
|
|
19
|
+
|
|
20
|
+
// zod v3's classic API assigns `.parse`/`.safeParse`/`.parseAsync`/
|
|
21
|
+
// `.safeParseAsync` as bound-per-instance COPIES of shared `ZodType.prototype`
|
|
22
|
+
// methods (`this.safeParse = this.safeParse.bind(this)` in the constructor),
|
|
23
|
+
// unlike v4's independent per-instance closures - so the hook target here is
|
|
24
|
+
// the prototype itself, patched before any schema is constructed (dep-hooks
|
|
25
|
+
// patches at module-load time, before app code can call `z.string()`).
|
|
26
|
+
//
|
|
27
|
+
// Only `safeParse`/`safeParseAsync` are patched, NOT `parse`/`parseAsync`.
|
|
28
|
+
// In v3's own source, `parse()` is `{ const result = this.safeParse(data,
|
|
29
|
+
// params); if (result.success) return result.data; throw result.error; }`,
|
|
30
|
+
// and `parseAsync()` wraps `safeParseAsync()` the same way - they are thin
|
|
31
|
+
// wrappers, not independent implementations. Patching all 4 (mirroring v4)
|
|
32
|
+
// would fire this hook TWICE per `.parse()` call - once via the outer patch,
|
|
33
|
+
// once via the inner `safeParse` call `parse()` makes internally - causing
|
|
34
|
+
// duplicate `tracker.track()` calls and duplicate propagation-graph nodes on
|
|
35
|
+
// every retracking check (trim/toLowerCase/transform). Patching only the two
|
|
36
|
+
// primitives avoids this: `parse`/`parseAsync` pick up tagging for free since
|
|
37
|
+
// they read `result.data` off the now-mutated safeParse/safeParseAsync return
|
|
38
|
+
// value. The `spa` alias (`safeParseAsync` alias, bound from the same
|
|
39
|
+
// prototype method) is covered for free too.
|
|
40
|
+
//
|
|
41
|
+
// Known gap: `ZodType.prototype['~validate']` (the Standard Schema v1 entry
|
|
42
|
+
// point used by tRPC v11, @hookform/resolvers, and newer
|
|
43
|
+
// fastify-type-provider-zod) calls `_parseSync` directly, bypassing
|
|
44
|
+
// `safeParse` entirely - uninstrumented here. v4 has the identical gap via
|
|
45
|
+
// its own `~standard.validate`. See NODE-4022 for a follow-up covering both.
|
|
46
|
+
const PARSE_METHODS = ['safeParse', 'safeParseAsync'];
|
|
47
|
+
|
|
48
|
+
module.exports = function(core) {
|
|
49
|
+
const {
|
|
50
|
+
depHooks,
|
|
51
|
+
patcher,
|
|
52
|
+
logger,
|
|
53
|
+
assess: { getPropagatorContext },
|
|
54
|
+
} = core;
|
|
55
|
+
const { handleParseSuccess } = require('./process-result-v3')(core);
|
|
56
|
+
|
|
57
|
+
function patchZodType(types) {
|
|
58
|
+
const proto = types?.ZodType?.prototype;
|
|
59
|
+
if (!proto) return;
|
|
60
|
+
|
|
61
|
+
PARSE_METHODS.forEach((method) => {
|
|
62
|
+
const isAsync = method.endsWith('Async');
|
|
63
|
+
|
|
64
|
+
patcher.patch(proto, method, {
|
|
65
|
+
name: `zod.${method}`,
|
|
66
|
+
patchType,
|
|
67
|
+
usePerf: isAsync ? 'tbd' : 'sync',
|
|
68
|
+
post(data) {
|
|
69
|
+
try {
|
|
70
|
+
if (!getPropagatorContext()) return;
|
|
71
|
+
|
|
72
|
+
// patching a prototype method means the schema is the receiver
|
|
73
|
+
// (`data.obj`), not `data.args[0]` like v4's shared-module hook -
|
|
74
|
+
// `data.args` here is just `[input, params]`.
|
|
75
|
+
const schema = data.obj;
|
|
76
|
+
const [input] = data.args;
|
|
77
|
+
|
|
78
|
+
// both patched methods always return the uniform
|
|
79
|
+
// `{success, data|error}` shape, so - unlike v4 - there's no
|
|
80
|
+
// isSafe/isAsync result-shape branching needed here.
|
|
81
|
+
if (isAsync) {
|
|
82
|
+
data.result = data.result.then((outcome) => {
|
|
83
|
+
if (outcome?.success) {
|
|
84
|
+
outcome.data = handleParseSuccess(schema, input, outcome.data);
|
|
85
|
+
}
|
|
86
|
+
return outcome;
|
|
87
|
+
});
|
|
88
|
+
} else if (data.result?.success) {
|
|
89
|
+
data.result.data = handleParseSuccess(schema, input, data.result.data);
|
|
90
|
+
}
|
|
91
|
+
} catch (err) {
|
|
92
|
+
logger.error({ err }, 'zod v3 parse propagation hook failed');
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const zodV3Instrumentation = core.assess.dataflow.propagation.zodV3Instrumentation = {
|
|
100
|
+
install() {
|
|
101
|
+
// stable, dominant 3.x line - unchanged from early 3.x through 3.24.4.
|
|
102
|
+
depHooks.resolve({ name: 'zod', file: 'lib/types.js', version: '>=3.0.0 <3.25.0' }, patchZodType);
|
|
103
|
+
|
|
104
|
+
// covers both the tail of the 3.25.x prerelease line AND, more
|
|
105
|
+
// importantly, any app on zod v4 using the `zod/v3` compat subpath
|
|
106
|
+
// (the standard migration path off v3) - byte-equivalent ZodType
|
|
107
|
+
// shape, confirmed against the installed zod package's own
|
|
108
|
+
// `v3/types.cjs`. Deliberately excludes zod@3.25.0-3.25.75, a short
|
|
109
|
+
// prerelease window that used two other transitional file layouts
|
|
110
|
+
// (`dist/commonjs/types.js`, then `dist/cjs/v3/types.js`) - not worth
|
|
111
|
+
// two more registrations for versions nobody pins to.
|
|
112
|
+
depHooks.resolve({ name: 'zod', file: 'v3/types.cjs', version: '>=3.25.76 <5' }, patchZodType);
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
return zodV3Instrumentation;
|
|
117
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright: 2026 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 { patchType } = require('../../common');
|
|
19
|
+
|
|
20
|
+
// zod v4's classic API (`import { z } from 'zod'`) assigns `.parse`/
|
|
21
|
+
// `.safeParse`/`.parseAsync`/`.safeParseAsync` as PER-INSTANCE closures in
|
|
22
|
+
// each schema's constructor (a deliberate zod performance optimization -
|
|
23
|
+
// there is no shared prototype method to patch). Every one of those closures
|
|
24
|
+
// calls through to the shared `v4/classic/parse.cjs` module's own exports
|
|
25
|
+
// (e.g. `inst.parse = (data, params) => parse.parse(inst, data, params, ...)`)
|
|
26
|
+
// as a live property lookup on every call, so patching that module's exports
|
|
27
|
+
// object is the single centralized, correct hook point.
|
|
28
|
+
const PARSE_METHODS = ['parse', 'safeParse', 'parseAsync', 'safeParseAsync'];
|
|
29
|
+
|
|
30
|
+
module.exports = function(core) {
|
|
31
|
+
const {
|
|
32
|
+
depHooks,
|
|
33
|
+
patcher,
|
|
34
|
+
logger,
|
|
35
|
+
assess: { getPropagatorContext },
|
|
36
|
+
} = core;
|
|
37
|
+
const { handleParseSuccess } = require('./process-result-v4')(core);
|
|
38
|
+
|
|
39
|
+
const zodInstrumentation = core.assess.dataflow.propagation.zodInstrumentation = {
|
|
40
|
+
install() {
|
|
41
|
+
depHooks.resolve(
|
|
42
|
+
{ name: 'zod', file: 'v4/classic/parse.cjs', version: '>=4 <5' },
|
|
43
|
+
(parseExports) => {
|
|
44
|
+
PARSE_METHODS.forEach((method) => {
|
|
45
|
+
const isSafe = method.startsWith('safe');
|
|
46
|
+
const isAsync = method.endsWith('Async');
|
|
47
|
+
|
|
48
|
+
patcher.patch(parseExports, method, {
|
|
49
|
+
name: `zod.${method}`,
|
|
50
|
+
patchType,
|
|
51
|
+
usePerf: isAsync ? 'tbd' : 'sync',
|
|
52
|
+
post(data) {
|
|
53
|
+
try {
|
|
54
|
+
if (!getPropagatorContext()) return;
|
|
55
|
+
|
|
56
|
+
const [schema, input] = data.args;
|
|
57
|
+
|
|
58
|
+
// handleParseSuccess may retrack the value (trim/toLowerCase/
|
|
59
|
+
// transform produce a NEW string) - its return value must
|
|
60
|
+
// replace `data.result` (sync) or be spliced into a
|
|
61
|
+
// replacement promise (async) so the original caller of
|
|
62
|
+
// `.parse()`/`.parseAsync()` actually receives the tracked
|
|
63
|
+
// value, not zod's untracked one. `post` runs before the
|
|
64
|
+
// patcher returns `data.result` to the caller, so mutating
|
|
65
|
+
// it here (sync case) takes effect; for async, a promise's
|
|
66
|
+
// resolution value can't be swapped after the fact, so we
|
|
67
|
+
// replace `data.result` itself with a chained promise.
|
|
68
|
+
if (isAsync) {
|
|
69
|
+
data.result = data.result.then((outcome) => {
|
|
70
|
+
if (isSafe) {
|
|
71
|
+
if (outcome.success) {
|
|
72
|
+
outcome.data = handleParseSuccess(schema, input, outcome.data);
|
|
73
|
+
}
|
|
74
|
+
return outcome;
|
|
75
|
+
}
|
|
76
|
+
return handleParseSuccess(schema, input, outcome);
|
|
77
|
+
});
|
|
78
|
+
} else if (isSafe) {
|
|
79
|
+
if (data.result.success) {
|
|
80
|
+
data.result.data = handleParseSuccess(schema, input, data.result.data);
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
data.result = handleParseSuccess(schema, input, data.result);
|
|
84
|
+
}
|
|
85
|
+
} catch (err) {
|
|
86
|
+
logger.error({ err }, 'zod parse propagation hook failed');
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return zodInstrumentation;
|
|
97
|
+
};
|
|
@@ -25,7 +25,7 @@ const {
|
|
|
25
25
|
StringPrototypeToLowerCase
|
|
26
26
|
}
|
|
27
27
|
} = require('@contrast/common');
|
|
28
|
-
const { Core } = require('@contrast/core
|
|
28
|
+
const { Core } = require('@contrast/core');
|
|
29
29
|
|
|
30
30
|
module.exports = Core.makeComponent({
|
|
31
31
|
name: 'assess.dataflow.sources.handle',
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
18
|
-
const { kComponentName, ComponentBase } = require('@contrast/core
|
|
18
|
+
const { kComponentName, ComponentBase } = require('@contrast/core');
|
|
19
19
|
const {
|
|
20
20
|
InputType,
|
|
21
21
|
traverseValues,
|
|
@@ -47,7 +47,6 @@ module.exports = class SAPInstrumentation extends ComponentBase {
|
|
|
47
47
|
sourceContext.cdsDispatchHandled = true;
|
|
48
48
|
const cdsReq = data.args[0];
|
|
49
49
|
|
|
50
|
-
// this should also take care of cdsReq.req._data and cdsReq.data
|
|
51
50
|
if (cdsReq?.req?.body) {
|
|
52
51
|
assess.dataflow.sources.handle({
|
|
53
52
|
context: 'req.body',
|
|
@@ -75,6 +74,19 @@ module.exports = class SAPInstrumentation extends ComponentBase {
|
|
|
75
74
|
}
|
|
76
75
|
}
|
|
77
76
|
|
|
77
|
+
if (cdsReq?.data) {
|
|
78
|
+
assess.dataflow.sources.handle({
|
|
79
|
+
context: 'req.data',
|
|
80
|
+
data: cdsReq.data,
|
|
81
|
+
name: data.name,
|
|
82
|
+
inputType: InputType.BODY,
|
|
83
|
+
sourceContext,
|
|
84
|
+
stacktraceOpts: {
|
|
85
|
+
constructorOpt: data.hooked,
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
78
90
|
if (cdsReq?.params?.length) {
|
|
79
91
|
assess.dataflow.sources.handle({
|
|
80
92
|
context: 'req.params',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { InputType, set } = require('@contrast/common');
|
|
4
|
-
const Core = require('@contrast/core
|
|
4
|
+
const Core = require('@contrast/core');
|
|
5
5
|
const { patchType } = require('../common');
|
|
6
6
|
|
|
7
7
|
const COMPONENT_NAME = 'assess.dataflow.sources.fastifyWebsocketInstrumentation';
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
'use strict';
|
|
16
16
|
|
|
17
17
|
const { InputType, set } = require('@contrast/common');
|
|
18
|
-
const Core = require('@contrast/core
|
|
18
|
+
const Core = require('@contrast/core');
|
|
19
19
|
const { patchType } = require('../common');
|
|
20
20
|
|
|
21
21
|
const COMPONENT_NAME = 'assess.dataflow.sources.socketIoInstrumentation';
|
package/lib/event-factory.js
CHANGED
|
@@ -23,7 +23,7 @@ const {
|
|
|
23
23
|
primordials: { StringPrototypeMatch }
|
|
24
24
|
} = require('@contrast/common');
|
|
25
25
|
const { ConfigSource } = require('@contrast/config');
|
|
26
|
-
const { Core } = require('@contrast/core
|
|
26
|
+
const { Core } = require('@contrast/core');
|
|
27
27
|
|
|
28
28
|
const ANNOTATION_REGEX = /^(A|O|R|P|P\d+)$/;
|
|
29
29
|
const SOURCE_EVENT_MSG = 'Source event not created: %s';
|
package/lib/index.js
CHANGED
package/lib/policy.js
CHANGED
|
@@ -25,7 +25,7 @@ const {
|
|
|
25
25
|
set,
|
|
26
26
|
primordials: { ArrayPrototypeJoin, RegExpPrototypeTest }
|
|
27
27
|
} = require('@contrast/common');
|
|
28
|
-
const { Core } = require('@contrast/core
|
|
28
|
+
const { Core } = require('@contrast/core');
|
|
29
29
|
|
|
30
30
|
const ASSESS_RULES = Object.values({
|
|
31
31
|
...Rule,
|