Search code examples
javascriptnode.jsjoi

Joi extension for escaping html tags


I want to create extension for sanitizing string inputs and stripping off the html tags.

For this, I am using sanitize-html npm package.

This is what I have tried so far.

const sanitizeHtml = require('sanitize-html');

module.exports = function htmlStrip(joi) {
  return {
    type: 'htmlStrip',
    base: joi.string(),
    messages: {
      htmlStrip: 'Should not contain any html tags.',
    },
    validate(value, helpers) {
      const clean = sanitizeHtml(value, {
        allowedTags: [],
        allowedAttributes: {},
      });
      if (clean) {
        return clean;
      }
      return { value, errors: helpers.error('htmlStrip') };
    },
  };
};

But I am getting below error.

TypeError: Joi.string(...).trim(...).htmlStrip is not a function

I also have tried passing rules object as below, but still getting the same error.

const sanitizeHtml = require('sanitize-html');

module.exports = function htmlStrip(joi) {
  return {
    type: 'htmlStrip',
    base: joi.string(),
    messages: {
      htmlStrip: 'Should not contain any html tags.',
    },
    rules: {
      htmlStrip: {
        validate(params, value, state, options) {
          const clean = sanitizeHtml(value, {
            allowedTags: [],
            allowedAttributes: {},
          });
          if (clean) {
            return clean;
          }
          return this.createError('string.htmlStrip', { value }, state, options);
        },
      },
    },
  };
};

I was following the doc mentioned here.

This is how I am using the extended validator.

const Joi = require('@hapi/joi').extend(require('@hapi/joi-date')).extend(require('../utils/sanitize-html-joi'));

const validateAddressCreateData = (data) => {
  const schema = Joi.object({
    address: Joi.string().trim().htmlStrip().required(),
    label: Joi.string()
      .required(),
  });
  return schema.validate(data);
};

Solution

  • Not sure why you assumed that type of Joi.string().trim() is going to be the same as Joi, considering you extend Joi object, yet expect htmlStrip to be available on Joi.string().trim() result.

    Simple console.log for both types shows that they are different types.

    console.log(Object.keys(Joi)):

    [ '_types',
      'alternatives',
      'any',
      'array',
      'boolean',
      ...
      'htmlStrip' ]
    

    console.log(Object.keys(Joi.string().trim());:

    [ 'type',
      '$_root',
      '$_temp',
      '_ids',
      '_preferences',
      '_valids',
      '_invalids',
      '_rules',
      '_singleRules',
      '_refs',
      '_flags',
      '_cache',
      '$_terms',
      '$_super' ]
    

    trim's result $_super does not seem to contain any keys, so they're not really related.

    I believe if you want to pre-process your input and then use your htmlStrip, you'd have to do something like this:

    const sanitizeHtml = require('sanitize-html');
    
    module.exports = function htmlStrip(joi) {
      return {
        type: 'htmlStrip',
        base: joi.string(),
        messages: {
          htmlStrip: 'Should not contain any html tags.',
        },
        validate(value, helpers) {
          const clean = sanitizeHtml(value, {
            allowedTags: [],
            allowedAttributes: {},
          });
          if (clean == value) {
            return { clean, errors: [] };
          }
          return { value, errors: helpers.error('htmlStrip') };
        },
      };
    };
    

    Here's how I'm using it:

    const Joi = require('@hapi/joi').extend(require('@hapi/joi-date')).extend(require('./san'));
    
    const validateAddressCreateData = (data) => {
      const schema = Joi.object({
        address: Joi.htmlStrip(Joi.string().trim()).required(),
        label: Joi.string().required(),
      });
      return schema.validate(data);
    };
    
    console.log(validateAddressCreateData({ address: "<body>test</body>", label: "abc"}));
    

    But I'm not sure if the output is as you expect it to be:

    { value: { address: '<body>test</body>', label: 'abc' },
      error:
       { ValidationError: Should not contain any html tags.
         _original: { address: '<body>test</body>', label: 'abc' },
         details: [ [Object] ] } }
    

    So, it seems to validate the input according to the validation message defined. Or do you actually want to modify the address html being passed?

    Edit got your point now, modified htmlStrip accordingly. Please see the comparison if(value == clean). In case the sanitizer didn't have to trim anything, it means that input string does not contain any html tags. Return error in other case.