fastify 5.12.0 → 5.12.2

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.
@@ -638,11 +638,14 @@ const fastify = require('fastify')({
638
638
  + `string`: Trust only given IP/CIDR (e.g. `'127.0.0.1'`). May be a list of
639
639
  comma separated values (e.g. `'127.0.0.1,192.168.1.1/24'`).
640
640
  + `Array<string>`: Trust only given IP/CIDR list (e.g. `['127.0.0.1']`).
641
- + `number`: Trust the nth hop from the front-facing proxy server as the client.
641
+ + `number`: Hop-count-only trust is disabled because it cannot validate the
642
+ immediate peer and lets direct clients spoof `X-Forwarded-*` values. Use an
643
+ IP/CIDR string, array, or custom function that validates the proxy address
644
+ instead.
642
645
  + `Function`: Custom trust function that takes `address` as first argument
643
646
  ```js
644
647
  function myTrustFn(address, hop) {
645
- return address === '1.2.3.4' || hop === 1
648
+ return address === '1.2.3.4' && hop === 0
646
649
  }
647
650
  ```
648
651
 
@@ -664,7 +667,9 @@ You may access the `ip`, `ips`, `host` and `protocol` values on the
664
667
  > These values are derived from socket/forwarding metadata and must be treated
665
668
  > as untrusted input unless your proxy chain is explicitly trusted and
666
669
  > validated. Do not use them directly for authorization or other
667
- > security-sensitive decisions without explicit validation.
670
+ > security-sensitive decisions without explicit validation. Custom trust
671
+ > functions must validate the `address` argument; hop-count-only checks such as
672
+ > `hop < 1` are unsafe when the Fastify origin can be reached directly.
668
673
 
669
674
  ```js
670
675
  fastify.get('/', (request, reply) => {
@@ -628,6 +628,17 @@ This pattern ensures validators work correctly with both sync and async
628
628
  `preValidation` hooks, preventing unhandled promise rejections that can crash
629
629
  an application.
630
630
 
631
+ > [!IMPORTANT]
632
+ > The `{ value, error }` result convention applies to **synchronous** validator
633
+ > compilers only. An **asynchronous** validator (a compiler that returns a
634
+ > promise, including an [`$async` Ajv schema](https://ajv.js.org/guide/async-validation.html))
635
+ > is treated as pass/fail only: its resolved value is not unwrapped as
636
+ > `{ value, error }`. An async schema resolves with the validated data itself, so
637
+ > unwrapping it would let a payload that carries `value` or `error` keys replace
638
+ > the request part or inject a validation error. An async validator must signal
639
+ > failure by rejecting (throwing), and it cannot coerce the request part through
640
+ > a resolved `value`.
641
+
631
642
  ##### .statusCode property
632
643
 
633
644
  All validation errors have a `.statusCode` property set to `400`, ensuring the
@@ -8,6 +8,9 @@
8
8
  - [FSTWRN001](#FSTWRN001)
9
9
  - [FSTWRN003](#FSTWRN003)
10
10
  - [FSTWRN004](#FSTWRN004)
11
+ - [Fastify Security Codes](#fastify-security-codes)
12
+ - [FSTSEC001](#FSTSEC001)
13
+ - [FSTSEC002](#FSTSEC002)
11
14
  - [Fastify Deprecation Codes](#fastify-deprecation-codes)
12
15
  - [FSTDEP022](#FSTDEP022)
13
16
  - [FSTDEP023](#FSTDEP023)
@@ -47,6 +50,14 @@ Disabling warnings is not recommended and may cause unexpected behavior.
47
50
  | <a id="FSTWRN001">FSTWRN001</a> | The specified schema for a route is missing. This may indicate the schema is not well specified. | Check the schema for the route. | [#4647](https://github.com/fastify/fastify/pull/4647) |
48
51
  | <a id="FSTWRN003">FSTWRN003</a> | The `%s` plugin mixes async and callback styles, which may lead to unhandled rejections. | Do not mix async and callback style. | [#6011](https://github.com/fastify/fastify/pull/6011) |
49
52
  | <a id="FSTWRN004">FSTWRN004</a> | An `errorHandler` is being overridden in the same scope, which can lead to subtle bugs. | Avoid calling `setErrorHandler` more than once in the same scope. For more information, see [Server documentation](https://fastify.dev/docs/latest/Reference/Server/#allowerrorhandleroverride). | [#6104](https://github.com/fastify/fastify/pull/6104) |
53
+
54
+ ### Fastify Security Codes
55
+
56
+ | Code | Description | How to solve | Discussion |
57
+ | ---- | ----------- | ------------ | ---------- |
58
+ | <a id="FSTSEC001">FSTSEC001</a> | A `RegExp` Content-Type is used that may be vulnerable to a CORS bypass, because it does not anchor the essence MIME type. | Start the `RegExp` with `^` or include `;?` so the essence MIME type is detected correctly. | [#4450](https://github.com/fastify/fastify/pull/4450) |
59
+ | <a id="FSTSEC002">FSTSEC002</a> | A headers schema references an external `$ref` (a schema registered with `addSchema`) that is not reached by header case normalization. Header names in the referenced schema keep their original case and do not match the lowercased request headers, so case-insensitive assertions such as `required` and `dependencies` may not apply. | Inline the header schema instead of referencing it with an external `$ref`. | - |
60
+
50
61
  ### Fastify Deprecation Codes
51
62
 
52
63
  Deprecation codes are supported by the Node.js CLI options:
package/fastify.d.ts CHANGED
@@ -149,7 +149,7 @@ declare namespace fastify {
149
149
  requestIdLogLabel?: string;
150
150
  useSemicolonDelimiter?: boolean,
151
151
  genReqId?: (req: RawRequestDefaultExpression<RawServer>) => string,
152
- trustProxy?: boolean | string | string[] | number | TrustProxyFunction,
152
+ trustProxy?: boolean | string | string[] | TrustProxyFunction,
153
153
  querystringParser?: (str: string) => { [key: string]: unknown },
154
154
  constraints?: {
155
155
  [name: string]: ConstraintStrategy<FindMyWayVersion<RawServer>, unknown>,
package/fastify.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const VERSION = '5.12.0'
3
+ const VERSION = '5.12.1'
4
4
 
5
5
  const Avvio = require('avvio')
6
6
  const http = require('node:http')
@@ -601,7 +601,7 @@ function fastify (serverOptions) {
601
601
 
602
602
  if (name === 'onClose') {
603
603
  this.onClose(fn.bind(this))
604
- } else if (name === 'onReady' || name === 'onListen' || name === 'onRoute') {
604
+ } else if (name === 'onReady' || name === 'onListen' || name === 'onRoute' || name === 'preClose') {
605
605
  this[kHooks].add(name, fn)
606
606
  } else {
607
607
  this.after((err, done) => {
@@ -34,11 +34,10 @@ function fourOhFour (options) {
34
34
 
35
35
  // 404 router, used for handling encapsulated 404 handlers
36
36
  const router = FindMyWay({
37
- onBadUrl: createRouteEventHandler(),
38
- onMaxParamLength: createRouteEventHandler(),
37
+ onBadUrl: options.routerOptions.onBadUrl,
38
+ onMaxParamLength: options.routerOptions.onMaxParamLength,
39
39
  defaultRoute: fourOhFourFallBack
40
40
  })
41
- let _routeEventHandler = null
42
41
 
43
42
  return { router, setNotFoundHandler, setContext, arrange404 }
44
43
 
@@ -47,8 +46,6 @@ function fourOhFour (options) {
47
46
  instance[kFourOhFourLevelInstance] = instance
48
47
  instance[kCanSetNotFoundHandler] = true
49
48
  // we need to bind instance for the context
50
- router.onBadUrl = router.onBadUrl.bind(instance)
51
- router.onMaxParamLength = router.onMaxParamLength.bind(instance)
52
49
  router.defaultRoute = router.defaultRoute.bind(instance)
53
50
  }
54
51
 
@@ -62,20 +59,15 @@ function fourOhFour (options) {
62
59
  })
63
60
  }
64
61
 
65
- function createRouteEventHandler () {
66
- return function onRouteEvent (path, req, res) {
67
- const fourOhFourContext = this[kFourOhFourLevelInstance][kFourOhFourContext]
68
- const id = getGenReqId(fourOhFourContext.server, req)
69
- const childLogger = createChildLogger(fourOhFourContext, logger, req, id)
70
- const request = new Request(id, null, req, null, childLogger, fourOhFourContext)
71
- const reply = new Reply(res, request, childLogger)
72
-
73
- _routeEventHandler(request, reply)
74
- }
75
- }
76
-
77
62
  function setContext (instance, context) {
78
- const _404Context = Object.assign({}, instance[kFourOhFourContext])
63
+ // Use the selected not-found context as the prototype rather than taking a
64
+ // detached shallow snapshot. The selected context's lifecycle hooks are
65
+ // populated during preReady (potentially after this snapshot is taken when
66
+ // the route is registered first), and prototype lookup keeps the
67
+ // route-specific 404 context linked to the live one so hooks like
68
+ // preHandler are not lost. The route's own onSend is preserved as an own
69
+ // property.
70
+ const _404Context = Object.create(instance[kFourOhFourContext])
79
71
  _404Context.onSend = context.onSend
80
72
  context[kFourOhFourContext] = _404Context
81
73
  }
@@ -123,12 +115,8 @@ function fourOhFour (options) {
123
115
  if (handler) {
124
116
  this[kFourOhFourLevelInstance][kCanSetNotFoundHandler] = false
125
117
  handler = handler.bind(this)
126
- // update route event handler
127
- _routeEventHandler = handler
128
118
  } else {
129
119
  handler = basic404
130
- // update route event handler
131
- _routeEventHandler = basic404
132
120
  }
133
121
 
134
122
  this.after((notHandledErr, done) => {
package/lib/request.js CHANGED
@@ -49,8 +49,9 @@ function getTrustProxyFn (tp) {
49
49
  return function () { return true }
50
50
  }
51
51
  if (typeof tp === 'number') {
52
- // Support trusting hop count
53
- return function (a, i) { return i < tp }
52
+ // Hop-count-only trust cannot validate the immediate peer. Fail closed so
53
+ // direct clients cannot spoof X-Forwarded-* values by supplying enough hops.
54
+ return function () { return false }
54
55
  }
55
56
  if (typeof tp === 'string') {
56
57
  // Support comma-separated tps
package/lib/route.js CHANGED
@@ -290,6 +290,7 @@ function buildRouting (options) {
290
290
  opts.attachValidation = false
291
291
  }
292
292
 
293
+ const routeConfigUrl = prefixing ? prefix : url
293
294
  if (prefixing === false) {
294
295
  // run 'onRoute' hooks
295
296
  for (const hook of this[kHooks].onRoute) {
@@ -328,7 +329,7 @@ function buildRouting (options) {
328
329
  const constraints = opts.constraints || {}
329
330
  const config = {
330
331
  ...opts.config,
331
- url,
332
+ url: routeConfigUrl,
332
333
  method: opts.method
333
334
  }
334
335
 
@@ -410,10 +411,13 @@ function buildRouting (options) {
410
411
  context.schema = normalizeSchema(context.schema, this.initialConfig)
411
412
 
412
413
  const schemaController = this[kSchemaController]
413
- const hasValidationSchema = opts.schema.body ||
414
- opts.schema.headers ||
415
- opts.schema.querystring ||
416
- opts.schema.params
414
+ // A JSON Schema boolean (`false` or `true`) is a valid schema and
415
+ // must still trigger the validator compiler setup, so presence is
416
+ // determined from an explicit `undefined` check rather than truthiness.
417
+ const hasValidationSchema = opts.schema.body !== undefined ||
418
+ opts.schema.headers !== undefined ||
419
+ opts.schema.querystring !== undefined ||
420
+ opts.schema.params !== undefined
417
421
  if (!opts.validatorCompiler && hasValidationSchema) {
418
422
  schemaController.setupValidator(this[kOptions])
419
423
  }
package/lib/schemas.js CHANGED
@@ -61,9 +61,12 @@ function normalizeSchema (routeSchemas, serverOptions) {
61
61
  }
62
62
 
63
63
  // alias query to querystring schema
64
- if (routeSchemas.query) {
64
+ // A JSON Schema boolean (`false` or `true`) is a valid schema and must still
65
+ // be aliased, so presence is determined from an explicit `undefined` check
66
+ // rather than truthiness.
67
+ if (routeSchemas.query !== undefined) {
65
68
  // check if our schema has both querystring and query
66
- if (routeSchemas.querystring) {
69
+ if (routeSchemas.querystring !== undefined) {
67
70
  throw new FST_ERR_SCH_DUPLICATE('querystring')
68
71
  }
69
72
  routeSchemas.querystring = routeSchemas.query
package/lib/validation.js CHANGED
@@ -13,7 +13,7 @@ const {
13
13
  FST_ERR_SCH_RESPONSE_SCHEMA_NOT_NESTED_2XX
14
14
  } = require('./errors')
15
15
 
16
- const { FSTWRN001 } = require('./warnings')
16
+ const { FSTWRN001, FSTSEC002 } = require('./warnings')
17
17
 
18
18
  function compileSchemasForSerialization (context, compile) {
19
19
  if (!context.schema || !context.schema.response) {
@@ -54,6 +54,169 @@ function compileSchemasForSerialization (context, compile) {
54
54
  }, {})
55
55
  }
56
56
 
57
+ // Header names are case insensitive (RFC 9110 §5.1), and Node.js stores every
58
+ // received header name in lowercase in `request.headers` (see the Node.js
59
+ // `message.headers` docs). The header schema must be normalized the same way
60
+ // before compilation, otherwise schema keywords that reference header names by
61
+ // their canonical or mixed case — `properties`, `required`, and the Draft 7
62
+ // `dependencies` keyword (triggers and array-form dependent names) — never
63
+ // match the lowercased request object, silently skipping assertions.
64
+ //
65
+ // JSON Schema is recursive, so the transformation walks the whole schema and
66
+ // lowercases every position where a property name (i.e. a header name) appears,
67
+ // while recursing into subschemas. This covers `properties` and `required` in
68
+ // every nested subschema, `dependencies` trigger keys, array-form dependent
69
+ // property names and subschemas used as dependency values, and subschemas
70
+ // reachable through `allOf`, `anyOf`, `oneOf`, `not`, `if`/`then`/`else`, local
71
+ // `$ref` definitions (`definitions`/`$defs`), and the other standard subschema
72
+ // containers. The input schema is never mutated: a new schema tree is returned.
73
+ function lowerCaseHeadersSchema (schema) {
74
+ if (Array.isArray(schema)) {
75
+ return schema.map(lowerCaseHeadersSchema)
76
+ }
77
+ if (schema === null || typeof schema !== 'object') {
78
+ return schema
79
+ }
80
+
81
+ const result = {}
82
+
83
+ for (const key of Object.keys(schema)) {
84
+ const value = schema[key]
85
+
86
+ switch (key) {
87
+ case 'properties': {
88
+ if (value === null || typeof value !== 'object') {
89
+ result.properties = value
90
+ break
91
+ }
92
+ const normalized = {}
93
+ for (const prop of Object.keys(value)) {
94
+ normalized[prop.toLowerCase()] = lowerCaseHeadersSchema(value[prop])
95
+ }
96
+ result.properties = normalized
97
+ break
98
+ }
99
+ case 'required':
100
+ result.required = Array.isArray(value)
101
+ ? value.map(name => name.toLowerCase())
102
+ : value
103
+ break
104
+ case 'dependencies': {
105
+ if (value === null || typeof value !== 'object') {
106
+ result.dependencies = value
107
+ break
108
+ }
109
+ const normalized = {}
110
+ for (const dep of Object.keys(value)) {
111
+ const depValue = value[dep]
112
+ if (Array.isArray(depValue)) {
113
+ normalized[dep.toLowerCase()] = depValue.map(name => name.toLowerCase())
114
+ } else {
115
+ normalized[dep.toLowerCase()] = lowerCaseHeadersSchema(depValue)
116
+ }
117
+ }
118
+ result.dependencies = normalized
119
+ break
120
+ }
121
+ case 'dependentSchemas': {
122
+ if (value === null || typeof value !== 'object') {
123
+ result.dependentSchemas = value
124
+ break
125
+ }
126
+ const normalized = {}
127
+ for (const dep of Object.keys(value)) {
128
+ normalized[dep.toLowerCase()] = lowerCaseHeadersSchema(value[dep])
129
+ }
130
+ result.dependentSchemas = normalized
131
+ break
132
+ }
133
+ case 'dependentRequired': {
134
+ if (value === null || typeof value !== 'object') {
135
+ result.dependentRequired = value
136
+ break
137
+ }
138
+ const normalized = {}
139
+ for (const dep of Object.keys(value)) {
140
+ normalized[dep.toLowerCase()] = value[dep].map(name => name.toLowerCase())
141
+ }
142
+ result.dependentRequired = normalized
143
+ break
144
+ }
145
+ // Arrays of subschemas (`allOf`/`anyOf`/`oneOf`) or a single subschema
146
+ // (`not`/`if`/`then`/`else`/`items`/`additionalItems`/...). `lowerCaseHeadersSchema`
147
+ // handles arrays, objects and booleans, so recursing here covers them all.
148
+ case 'allOf':
149
+ case 'anyOf':
150
+ case 'oneOf':
151
+ case 'not':
152
+ case 'if':
153
+ case 'then':
154
+ case 'else':
155
+ case 'items':
156
+ case 'additionalItems':
157
+ case 'additionalProperties':
158
+ case 'unevaluatedItems':
159
+ case 'unevaluatedProperties':
160
+ case 'contains':
161
+ case 'propertyNames':
162
+ case 'contentSchema':
163
+ result[key] = lowerCaseHeadersSchema(value)
164
+ break
165
+ // Maps of subschemas whose keys are not header names (definition names,
166
+ // regex patterns): keep the keys, normalize the subschema values.
167
+ case 'definitions':
168
+ case '$defs':
169
+ case 'patternProperties': {
170
+ if (value === null || typeof value !== 'object') {
171
+ result[key] = value
172
+ break
173
+ }
174
+ const normalized = {}
175
+ for (const k of Object.keys(value)) {
176
+ normalized[k] = lowerCaseHeadersSchema(value[k])
177
+ }
178
+ result[key] = normalized
179
+ break
180
+ }
181
+ default:
182
+ result[key] = value
183
+ }
184
+ }
185
+
186
+ return result
187
+ }
188
+
189
+ // `lowerCaseHeadersSchema` only normalizes the inline schema tree. A header
190
+ // schema can also pull in a schema registered with `addSchema()` through an
191
+ // external `$ref` (any ref that does not start with `#`, i.e. not a same
192
+ // document fragment). Those referenced schemas are resolved by the validator
193
+ // after normalization and keep their original header-name case, so
194
+ // case-insensitive assertions in them never match the lowercased request
195
+ // headers. We cannot safely normalize them here because the same registered
196
+ // schema is shared with body, querystring and params validation, so we detect
197
+ // the external reference and warn the developer instead. Returns the first
198
+ // external `$ref` found, or undefined.
199
+ function findExternalHeaderRef (schema) {
200
+ if (Array.isArray(schema)) {
201
+ for (const item of schema) {
202
+ const ref = findExternalHeaderRef(item)
203
+ if (ref !== undefined) return ref
204
+ }
205
+ return undefined
206
+ }
207
+ if (schema === null || typeof schema !== 'object') {
208
+ return undefined
209
+ }
210
+ if (typeof schema.$ref === 'string' && schema.$ref[0] !== '#') {
211
+ return schema.$ref
212
+ }
213
+ for (const key of Object.keys(schema)) {
214
+ const ref = findExternalHeaderRef(schema[key])
215
+ if (ref !== undefined) return ref
216
+ }
217
+ return undefined
218
+ }
219
+
57
220
  function compileSchemasForValidation (context, compile, isCustom) {
58
221
  const { schema } = context
59
222
  if (!schema) {
@@ -62,32 +225,37 @@ function compileSchemasForValidation (context, compile, isCustom) {
62
225
 
63
226
  const { method, url } = context.config || {}
64
227
 
228
+ // JSON Schema Draft 7 defines the boolean `false` (and `true`) as a valid
229
+ // schema. A present boolean must be passed to the compiler unchanged, so
230
+ // every request-part schema is selected by an explicit `undefined` check
231
+ // rather than truthiness. FSTWRN001 is preserved only for schemas that are
232
+ // explicitly set to `undefined`.
233
+
65
234
  const headers = schema.headers
66
235
  // the or part is used for backward compatibility
67
- if (headers && (isCustom || Object.getPrototypeOf(headers) !== Object.prototype)) {
68
- // do not mess with schema when custom validator applied, e.g. Joi, Typebox
69
- context[headersSchema] = compile({ schema: headers, method, url, httpPart: 'headers' })
70
- } else if (headers) {
71
- // The header keys are case insensitive
72
- // https://datatracker.ietf.org/doc/html/rfc2616#section-4.2
73
- const headersSchemaLowerCase = {}
74
- Object.keys(headers).forEach(k => { headersSchemaLowerCase[k] = headers[k] })
75
- if (headersSchemaLowerCase.required instanceof Array) {
76
- headersSchemaLowerCase.required = headersSchemaLowerCase.required.map(h => h.toLowerCase())
77
- }
78
- if (headers.properties) {
79
- headersSchemaLowerCase.properties = {}
80
- Object.keys(headers.properties).forEach(k => {
81
- headersSchemaLowerCase.properties[k.toLowerCase()] = headers.properties[k]
82
- })
236
+ if (headers !== undefined) {
237
+ if (isCustom || typeof headers !== 'object' || headers === null || Object.getPrototypeOf(headers) !== Object.prototype) {
238
+ // do not mess with schema when custom validator applied, e.g. Joi, Typebox
239
+ // boolean schemas (true/false) and invalid values are compiled directly
240
+ context[headersSchema] = compile({ schema: headers, method, url, httpPart: 'headers' })
241
+ } else {
242
+ // The header keys are case insensitive
243
+ // https://datatracker.ietf.org/doc/html/rfc2616#section-4.2
244
+ const headersSchemaLowerCase = lowerCaseHeadersSchema(headers)
245
+ const externalRef = findExternalHeaderRef(headers)
246
+ if (externalRef !== undefined) {
247
+ FSTSEC002(method, url, externalRef)
248
+ }
249
+ context[headersSchema] = compile({ schema: headersSchemaLowerCase, method, url, httpPart: 'headers' })
83
250
  }
84
- context[headersSchema] = compile({ schema: headersSchemaLowerCase, method, url, httpPart: 'headers' })
85
251
  } else if (Object.hasOwn(schema, 'headers')) {
86
252
  FSTWRN001('headers', method, url)
87
253
  }
88
254
 
89
- if (schema.body) {
90
- const contentProperty = schema.body.content
255
+ if (schema.body !== undefined) {
256
+ const contentProperty = schema.body !== null && typeof schema.body === 'object'
257
+ ? schema.body.content
258
+ : undefined
91
259
  if (contentProperty) {
92
260
  const contentTypeSchemas = {}
93
261
  for (const contentType of Object.keys(contentProperty)) {
@@ -102,13 +270,13 @@ function compileSchemasForValidation (context, compile, isCustom) {
102
270
  FSTWRN001('body', method, url)
103
271
  }
104
272
 
105
- if (schema.querystring) {
273
+ if (schema.querystring !== undefined) {
106
274
  context[querystringSchema] = compile({ schema: schema.querystring, method, url, httpPart: 'querystring' })
107
275
  } else if (Object.hasOwn(schema, 'querystring')) {
108
276
  FSTWRN001('querystring', method, url)
109
277
  }
110
278
 
111
- if (schema.params) {
279
+ if (schema.params !== undefined) {
112
280
  context[paramsSchema] = compile({ schema: schema.params, method, url, httpPart: 'params' })
113
281
  } else if (Object.hasOwn(schema, 'params')) {
114
282
  FSTWRN001('params', method, url)
@@ -120,7 +288,16 @@ function validateParam (validatorFunction, request, paramName) {
120
288
  let ret
121
289
 
122
290
  try {
123
- ret = validatorFunction?.(isUndefined ? null : request[paramName])
291
+ const data = isUndefined ? null : request[paramName]
292
+ // Ajv can only mutate a root value when its parent is provided.
293
+ if (validatorFunction?.schemaEnv) {
294
+ ret = validatorFunction(data, {
295
+ parentData: request,
296
+ parentDataProperty: paramName
297
+ })
298
+ } else {
299
+ ret = validatorFunction?.(data)
300
+ }
124
301
  } catch (err) {
125
302
  // If validator throws synchronously, ensure it propagates as an internal error
126
303
  err.statusCode = 500
@@ -128,8 +305,13 @@ function validateParam (validatorFunction, request, paramName) {
128
305
  }
129
306
 
130
307
  if (ret && typeof ret.then === 'function') {
308
+ // An async validator (for example an `$async` Ajv schema) resolves with the
309
+ // validated data itself, not a `{ value, error }` result. Its properties must
310
+ // not be unwrapped the way a synchronous custom compiler result is, otherwise
311
+ // a payload carrying an `error` or `value` key could inject a validation error
312
+ // or replace the whole request part. Treat the async result as pass/fail only.
131
313
  return ret
132
- .then((res) => { return answer(res) })
314
+ .then((res) => { return res === false ? validatorFunction.errors : false })
133
315
  .catch(err => { return err }) // return as simple error (not throw)
134
316
  }
135
317
 
package/lib/warnings.js CHANGED
@@ -3,9 +3,10 @@
3
3
  const { createWarning } = require('process-warning')
4
4
 
5
5
  /**
6
- * Deprecation codes:
6
+ * Warning codes:
7
7
  * - FSTWRN001
8
8
  * - FSTSEC001
9
+ * - FSTSEC002
9
10
  * - FSTDEP022
10
11
  * - FSTDEP023
11
12
  * - FSTDEP024
@@ -44,6 +45,13 @@ const FSTSEC001 = createWarning({
44
45
  unlimited: true
45
46
  })
46
47
 
48
+ const FSTSEC002 = createWarning({
49
+ name: 'FastifySecurity',
50
+ code: 'FSTSEC002',
51
+ message: 'The headers schema for %s: %s references an external $ref (%s) that is not case-normalized. Header names in the referenced schema keep their original case and will not match the lowercased request headers, so case-insensitive assertions such as required and dependencies may not apply. Inline the header schema instead of referencing it with an external $ref.',
52
+ unlimited: true
53
+ })
54
+
47
55
  const FSTDEP022 = createWarning({
48
56
  name: 'FastifyWarning',
49
57
  code: 'FSTDEP022',
@@ -77,6 +85,7 @@ module.exports = {
77
85
  FSTWRN003,
78
86
  FSTWRN004,
79
87
  FSTSEC001,
88
+ FSTSEC002,
80
89
  FSTDEP022,
81
90
  FSTDEP023,
82
91
  FSTDEP024,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify",
3
- "version": "5.12.0",
3
+ "version": "5.12.2",
4
4
  "description": "Fast and low overhead web framework, for Node.js",
5
5
  "main": "fastify.js",
6
6
  "type": "commonjs",