hapijs / joi

The most powerful data validation library for JS
Other
20.95k stars 1.51k forks source link

How to update input before validate? #2977

Closed qwang07 closed 1 year ago

qwang07 commented 1 year ago

Support plan

Context

How can we help?

In my project, some of the inputs are mixed with different types of input. The object to be validated is sent as a string, so I need to convert them to JSON using the JSON.parse method before validation. Is there any way in Joi to convert these inputs before validation?

// something like this
const schema = Joi.convert(input => JSON.parse(input)).object({ name: Joi.string, age: Joi.number() });
qwang07 commented 1 year ago

I tried something like this, but all error details are combined together:

import Joi from "joi"

const text = `{"b": "abc", "c": true}`
const schema = Joi.custom(jsonParse(Joi.object({
  a: Joi.string().required(),
  b: Joi.number(),
  c: Joi.bool(),
})))

const result = schema.validate(text, { abortEarly: false })

console.log(result.error)

function jsonParse(schema) {
  return (value) => {
    const json = JSON.parse(value)
    const result = schema.validate(json, { abortEarly: false })

    if (result.error) {
      console.log(result.error)
      throw result.error
    }

    return result.value
  }
}
// Output:
[Error [ValidationError]: "value" failed custom validation because "a" is required. "b" must be a number] {
  _original: '{"b": "abc", "c": true}',
  details: [
    {
      message: '"value" failed custom validation because "a" is required. "b" must be a number',
      path: [],
      type: 'any.custom',
      context: [Object]
    }
  ]
}
qwang07 commented 1 year ago

I have done this with extend().

import Joi from 'joi'

const custom = Joi.extend(
  {
    type: 'object',
    base: Joi.object(),
    coerce: {
      from: 'string',
      method(value) {
        if (value[0] !== '{' || !/^\s*\{/.test(value)) {
          return
        }

        try {
          return { value: JSON.parse(value) }
        } catch (ignoreErr) { }
      }
    }
  },
  {
    type: 'array',
    base: Joi.array(),
    coerce: {
      from: 'string',
      method(value) {
        if (value[0] !== '[' || !/^\s*\[/.test(value)) {
          return
        }

        try {
          return { value: JSON.parse(value) }
        } catch (ignoreErr) { }
      }
    }
  }
)

export default custom
Nargonath commented 1 year ago

@qwang07 In the past joi used to do this automatically for you (pre v16). However there are security concerns that were overlooked at that time that led to a security vulnerability. You can find more information about this here: https://fastify.dev/docs/latest/Guides/Prototype-Poisoning/ as this was written by Eran Hammer after discovering the issue and fixing it.

With the current solution you posted above, you open yourself to a potential prototype pollution attack on your server. There are other conditions for such attack to be successful but at least you satisfy already one of the criteria. Eran wrote some examples for people that want to bring back such feature as you did, you can find a screenshot of it here: https://github.com/hapijs/joi/issues/2129#issuecomment-533004092 along with a link to the source. You'll notice that he doesn't use JSON.parse but rather Bounce.parse for that specific reason. If you read the Fastify article, you'll understand why.

I hope this helps.

qwang07 commented 1 year ago

Thanks a lot

Nargonath commented 1 year ago

You're welcome. 😉