GithubHelp home page GithubHelp logo

typestack / class-sanitizer Goto Github PK

View Code? Open in Web Editor NEW
99.0 4.0 19.0 248 KB

Decorator based class property sanitation in Typescript.

License: MIT License

TypeScript 99.49% JavaScript 0.51%
typescript sanitizer

class-sanitizer's Introduction

class-sanitizer

Build Status codecov npm version

Decorator based class property sanitation in Typescript powered by validator.js.

DEPRECATION NOTICE:
This library is considered to be deprecated and won't be updated anymore. Please use the class-transformer and/or class-validator libraries instead.

Installation

npm install class-sanitizer --save

Usage

To start using the library simply create some classes and add some sanitization decorators to the properties. When calling sanitize(instance) the library will automatically apply the rules defined in the decorators to the properties and update the value of every marked property respectively.

NOTE:
Every sanitization decorator is property decorator meaning it cannot be placed on parameters or class definitions.

import { sanitize, Trim } from 'class-sanitizer';

class TestClass {
  @Trim()
  label: string;

  constructor(label: string) {
    this.label = label;
  }
}

const instance = new TestClass(' text-with-spaces-on-both-end ');

sanitize(instance);
// -> the label property is trimmed now
// -> { label: 'text-with-spaces-on-both-end' }

Validating arrays

Every decorator expects a SanitationOptions object. When the each property is set to true the array will be iterated and the decorator will be applied to every element of the array.

import { sanitize, Trim } from 'class-sanitizer';

class TestClass {
  @Trim(undefined, { each: true })
  labels: string[];

  constructor(labels: string[]) {
    this.labels = labels;
  }
}

const instance = new TestClass([' labelA ', ' labelB', 'labelC ']);

sanitize(instance);
// -> Every value is trimmed in instance.labels now.
// -> { labels: ['labelA', 'labelB', 'labelC']}

Inheritance

Class inheritance is supported, every decorator defined on the base-class will be applied to the property with same name on the descendant class if the property exists.

Note:
Only one level of inheritance is supported! So if you have ClassA inherit ClassB which inherits ClassC the decorators from ClassC won't be applied to ClassA when sanitizing.

import { sanitize, Trim } from 'class-sanitizer';

class BaseClass {
  @Trim()
  baseText: string;
}

class DescendantClass extends BaseClass {
  @Trim()
  descendantText: string;
}

const instance = new DescendantClass();
instance.baseText = ' text ';
instance.descendantText = ' text ';

sanitize(instance);
// -> Both value is trimmed now.
// -> { baseText: 'text', descendantText: 'text' }

Sanitizing nested values with @SanitizeNested() decorator

The @SanitizeNested property can be used to instruct the library to lookup the sanitization rules for the class instance found on the marked property and sanitize it.

import { sanitize, Trim, SanitizeNested } from 'class-sanitizer';

class InnerTestClass {
  @Trim()
  text: string;

  constructor(text: string) {
    this.text = text;
  }
}

class TestClass {
  @SanitizeNested({ each: true })
  children: InnerTestClass[];

  @SanitizeNested({ each: false })
  child: InnerTestClass;
}

const instance = new TestClass();
const innerA = new InnerTestClass(' innerA ');
const innerB = new InnerTestClass(' innerB ');
const innerC = new InnerTestClass(' innerC ');
instance.children = [innerA, innerB];
instance.child = innerC;

sanitize(instance);
// -> Both values in the array on `children` property and value on `child` property is sanitized.
// -> { children: [ { text: 'innerA' }, { text: 'innerB' }], child: { 'innerC' }}

Custom sanitation classes

The @SanitizerConstraint( decorator can be used to define custom sanitization logic. Creating a custom sanitization class requires the following steps:

  1. Create a class which implements the CustomSanitizer interface and decorate the class with the @SanitizerConstraint() decorator.

    import { CustomSanitizer, SanitizerConstraint } from 'class-sanitizer';
    import { Container } from 'typedi';
    
    @SanitizerConstraint()
    export class LetterReplacer implements CustomSanitizer {
      /** If you use TypeDI, you can inject services to properties with `Container.get` function. */
      someInjectedService = Container.get(SomeClass);
    
      /**
       * This function will be called during sanitization.
       *  1, It must be a sync function
       *  2, It must return the transformed value.
       */
    
      sanitize(text: string): string {
        return text.replace(/o/g, 'w');
      }
    }
  2. Then you can use your new sanitation constraint in your class:

    import { Sanitize } from 'class-sanitizer';
    import { LetterReplacer } from './LetterReplacer';
    
    export class Post {
      @Sanitize(LetterReplacer)
      title: string;
    }
  3. Now you can use sanitizer as usual:

    import { sanitize } from 'class-sanitizer';
    
    sanitize(post);

Manual sanitation

There are several method exist in the Sanitizer that allows to perform non-decorator based sanitation:

import Sanitizer from 'class-sanitizer';

Sanitizer.trim(` Let's trim this! `);

Sanitization decorators

The following property decorators are available.

Decorator Description
@Blacklist(chars: string) Removes all characters that appear in the blacklist.
@Whitelist(chars: string) Removes all characters that don't appear in the whitelist.
@Trim(chars?: string) Trims characters (whitespace by default) from both sides of the input. You can specify chars that should be trimmed.
@Ltrim(chars?: string) Trims characters from the left-side of the input.
@Rtrim(chars?: string) Trims characters from the right-side of the input.
@Escape() Replaces <, >, &, ', " and / with HTML entities.
@NormalizeEmail(lowercase?: boolean) Normalizes an email address.
@StripLow(keepNewLines?: boolean) Removes characters with a numerical value < 32 and 127, mostly control characters.
@ToBoolean(isStrict?: boolean) Converts the input to a boolean. Everything except for '0', 'false' and '' returns true. In strict mode only '1' and 'true' return true.
@ToDate() Converts the input to a date, or null if the input is not a date.
@ToFloat() Converts the input to a float, or NaN if the input is not an integer.
@ToInt(radix?: number) Converts the input to an integer, or NaN if the input is not an integer.
@ToString() Converts the input to a string.

class-sanitizer's People

Contributors

nonameprovided avatar yourit avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

class-sanitizer's Issues

Inherited properties are ignored

Sanitising a class that inherits properties from another class does not sanitise the inherit properties

class Class1 {
@ToInt()
id:number;
}

class Class2 extends Class1 {
@IsEmail()
email:string;
}

const instance = new Class2();
instance.email = '[email protected]'; // gets sanitised correctly
instance.id = 'Not_A_Number'; // does not get sanitised

sanitize(instance);

Probably the same as in class-validator: typestack/class-validator#31

Sanitizing a property is not limited to class where it is applied

When a decorator is applied to a field it is applied not only when validating instances of its class, but when validating any class.

Consider this:

export class A {
  message: string;
}

export class B {
  @Escape()
  message: string;
}

let a = new A();
a.message = '<';

sanitize(a);
console.log(a);

// Expected output: A { message: '<' }
// Actual output: A { message: '&lt;' }

I suspect that MetadataStorage.getSanitizeMetadatasForObject() is not working correctly.

Pass arguments to custom sanitizer

The docs describe how to create a custom sanitization class, but it seems impossible to add additional arguments. I'm trying to implement a Default sanitizer (see #11), but I'm hitting some limitations with decorators.

After seeing that there are no "official" additional arguments, my first intuition was to use a function which returns a class:

export default function (defaultValue: any) {
  return class implements SanitizerInterface {
    public sanitize (value: any): any {
      console.log('default sanitizer', value, args)
      return value === undefined ? defaultValue : value
    }
  }
}

However, I cannot apply the decorator to a class expression, only to class declarations. I cannot add the default value to constructor either because I'm not the one calling new.

I'll try to find a way to emulate applying decorator on the class expression instead of on a class declaration, but would like an official solution.

The "Default" sanitizer

Seems like a common use-case to want to add a default value if it's missing completely (undefined, possibly extensible to null by config).

class Query {
  @ToInt()
  @Default(1)
  page: number
}

const q = new Query() as any
sanitize(q) // { page: 1 }

Sanitizer for a property of one class is applied to another class when both classes inherit from a parent

I have the following TypeORM entities:

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
import { IsNotEmpty } from 'class-validator';
import { Trim } from 'class-sanitizer';
import { BaseEntity } from './BaseEntity';

/**
 * A event's label
 * @example democracy, philosophy
 */
@Entity()
export class EventLabel extends BaseEntity {
  @PrimaryGeneratedColumn('uuid') id: string;

  @Trim()
  @IsNotEmpty()
  @Column({ type: 'varchar', unique: true, nullable: false })
  text: string;

  @Column({ type: 'datetime', nullable: false })
  createdAt: Date;
}
import {
  Entity,
  Column,
  PrimaryGeneratedColumn,
  ManyToOne,
  OneToMany,
} from 'typeorm';
import { IsNotEmpty, ValidateIf, IsUrl } from 'class-validator';
import { BaseEntity } from './BaseEntity';
import { EditorialSummary } from './EditorialSummary';
import { EditorialSummaryNodeType } from '../../typings/schema';
import { urlValidationOptions } from '../../helpers/validation';

const nodeTypes: Record<EditorialSummaryNodeType, string> = {
  quote: '',
  heading: '',
  paragraph: '',
  text: '',
  link: '',
  emphasis: '',
};

export type NodeType = keyof typeof nodeTypes;

/**
 * Editorial content from the old Hollowverse website
 */
@Entity()
export class EditorialSummaryNode extends BaseEntity {
  @PrimaryGeneratedColumn('uuid') id: string;

  /**
   * The order of the editorial summary node in the
   * original text, used to reconstruct the text
   */
  @Column({
    nullable: false,
    type: 'smallint',
  })
  order: number;

  @Column({
    nullable: false,
    type: 'enum',
    default: null,
    enum: Object.keys(nodeTypes),
  })
  type: NodeType;

  @Column('varchar', {
    nullable: true,
    length: 1000,
  })
  @ValidateIf((_, v) => typeof v === 'string')
  @IsNotEmpty()
  text: string | null;

  @ValidateIf((_, v) => typeof v === 'string')
  @IsUrl(urlValidationOptions)
  @Column('varchar', {
    nullable: true,
    length: 1000,
  })
  sourceUrl: string | null;

  @Column({
    nullable: true,
    type: 'varchar',
  })
  sourceTitle: string | null;

  @ManyToOne(
    _ => EditorialSummary,
    editorialSUmmary => editorialSUmmary.nodes,
    {
      nullable: false,
    },
  )
  editorialSummary: EditorialSummary;

  @ManyToOne(_ => EditorialSummaryNode, node => node.children, {
    cascade: ['insert', 'update'],
  })
  parent: EditorialSummaryNode | null;

  @OneToMany(_ => EditorialSummaryNode, node => node.parent, {
    cascade: ['insert', 'update'],
  })
  children: EditorialSummaryNode[];
}

This is the base entity:

import { BeforeInsert, BeforeUpdate } from 'typeorm';
import { validateOrReject } from 'class-validator';
import { sanitizeAsync } from 'class-sanitizer';

/**
 * Base entity for the database layer.
 *
 * All entities should extend this class to automatically
 * perform validations on insertions and updates.
 */
export class BaseEntity {
  @BeforeInsert()
  async validate() {
    await sanitizeAsync(this);

    return validateOrReject(this);
  }

  @BeforeUpdate()
  async validateUpdate() {
    await sanitizeAsync(this);

    return validateOrReject(this, { skipMissingProperties: true });
  }
}

Both entities have a property named text, but only EventLabel.text should be trimmed. I was debugging typeorm/typeorm#1397 and I wondered what would happen if I removed all references of @Trim in the entire codebase, and it turns out that commenting the call to @Trim in EventLabel fixes the mentioned issue.

ToInt keeps empty string instead of changing to NaN

The ToInt sanitizer doesn't take into account empty string. Looks like a validator.js upstream issue, but since it's currently locked on version 5, which is five major versions behind, it's worth testing if a simple bump will work first. Otherwise this issue should be taken there.

class Query {
  @ToInt()
  readonly page: number
}

const q = new Query() as any
q.page = ''
sanitize(q)

console.log(q) // { page: '' }

Reproduction: https://stackblitz.com/edit/class-sanitizer-to-int-issue?file=index.ts

This happens with query parameters sent as &page=.

Only sanitize if condition is met

Consider this TypeORM entity:

import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
import { IsEmail, IsUrl, ValidateIf } from 'class-validator';
import { Trim } from 'class-sanitizer';
import { BaseEntity } from './base';

@Entity()
export class User extends BaseEntity {
  @PrimaryGeneratedColumn('uuid') id: string;

  @ValidateIf((_, v) => typeof v === 'string')
  @IsEmail({
    allow_display_name: false,
    require_tld: true,
  })
  @Column({ type: 'varchar', unique: true, nullable: true })
  email: string | null;

  @Trim()
  @Column({ type: 'varchar', nullable: false })
  name: string;

  @ValidateIf((_, v) => typeof v === 'string')
  @IsUrl({
    require_protocol: true,
    require_valid_protocol: true,
    protocols: ['https', 'http'],
  })
  @Column({ type: 'text', nullable: true })
  photoUrl: string | null;
}

Because the email address is nullable, there is currently no simple way to sanitize it. If the the value is null, and we use normalizeEmail, the entity will fail to be persisted in the database. It would be nice to have a decorator, sanitizeIf, similar to validateIf, that only calls the sanitization function when the value is a string.

Custom sanitation class called multiple times when used with typeorm entities.

I have tried this class with typeorm entities, like this:

export class Post{
    @PrimaryGeneratedColumn()
    post_id: number;

    @Sanitize(SanitizeHtml)
    @Column({type:'text',nullable:true})
    comment:string;

    @BeforeInsert()
    purifyInsert() {
        sanitize(this)
    }

    @BeforeUpdate()
    purifyUpdate(){
       sanitize(this)
    }
}

SanitizeHtml.ts

@SanitizerConstraint()
export class SanitizeHtml implements SanitizerInterface {
    sanitize(value: any): any {
        let val = DOMPurify.sanitize(value);
        console.log(val) 
        return val;
    }
}

When I run this code:

 let sell = new Post();
 sell.comment ="<p>asddsafdsaassdadsa</p>";
 sanitize(sell)

I found that the sanitizer was running 16 times in the console, which ,I believe, is caused by the other 15 entities also using the same custom sanitizer on columns with the same name comment.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.