"ADVERTENCIA: desinfectar HTML eliminó algún contenido" y cómo tratarlo correctamente

Todos los que han tenido que incrustar contenido HTML en el DOM en Angular han visto este mensaje. Por supuesto, todos obtenemos el contenido verificado de nuestro propio servidor y solo queremos cubrir el mensaje de error. O bien, incorporamos HTML desde nuestras constantes, alineamos nuestros íconos SVG, porque solo necesitamos colorearlos en el color del texto. Después de todo, nada malo sucederá si le decimos a Angular: no es una deriva, todo está limpio allí.


A menudo esto puede ser así, pero en proyectos grandes con una gran cantidad de desarrolladores que escriben componentes independientes, nunca se sabe con seguridad dónde puede estar su código. Y si usted, como yo, está desarrollando una biblioteca de componentes reutilizables, entonces esta situación debe resolverse de raíz.



Sanitize y DomSanitizer


Angular tiene clases abstractas cuyas implementaciones están diseñadas para borrar el contenido de basura maliciosa:


abstract class Sanitizer { abstract sanitize(context: SecurityContext, value: string | {}): string | null } 

 abstract class DomSanitizer implements Sanitizer { abstract sanitize(context: SecurityContext, value: string | SafeValue): string | null abstract bypassSecurityTrustHtml(value: string): SafeHtml abstract bypassSecurityTrustStyle(value: string): SafeStyle abstract bypassSecurityTrustScript(value: string): SafeScript abstract bypassSecurityTrustUrl(value: string): SafeUrl abstract bypassSecurityTrustResourceUrl(value: string): SafeResourceUrl } 

La clase interna específica DomSanitizerImpl Angular utiliza en la construcción del DOM. Es su gente quien agrega directivas, componentes y tuberías para decirle al marco que este contenido es confiable.


El código fuente de esta clase es bastante simple:
https://github.com/angular/angular/blob/8.1.0/packages/platform-browser/src/security/dom_sanitization_service.ts#L148


Este servicio introduce el concepto de SafeValue . En esencia, esto es solo una clase de contenedor para una cadena. Cuando Renderer inserta un valor a través del enlace, ya sea innerHTML , @HostBinding , style o src , el valor se ejecuta a través del método sanitize . Si SafeValue ya llegó allí, simplemente devuelve la cadena envuelta, de lo contrario, limpia el contenido por sí solo.


Angular no es una biblioteca especializada en limpieza de códigos de malware. Por lo tanto, el marco sigue correctamente el camino de menor riesgo y corta todo lo que causa preocupación. El código SVG se convierte en líneas vacías, se eliminan los estilos en línea, etc. Sin embargo, hay bibliotecas diseñadas solo para proteger el DOM, una de las cuales es DOMPurify:
https://github.com/cure53/DOMPurify



La tubería SafeHtml correcta


Después de conectar DOMPurify, podemos hacer una tubería que no solo marque el contenido como seguro, sino que también lo limpie. Para hacer esto, DOMPurify.sanitize valor de entrada a través del método DOMPurify.sanitize y luego DOMPurify.sanitize como seguro con el contexto apropiado:


 @Pipe({name: 'dompurify'}) export class NgDompurifyPipe implements PipeTransform { constructor(private readonly domSanitizer: DomSanitizer) {} transform( value: {} | string | null, context: SecurityContext = SecurityContext.HTML, ): SafeValue | null { return this.bypassSecurityTrust(context, DOMPurify.sanitize(value)); } private bypassSecurityTrust( context: SecurityContext, purifiedValue: string, ): SafeValue | null { switch (context) { case SecurityContext.HTML: return this.domSanitizer.bypassSecurityTrustHtml(purifiedValue); case SecurityContext.STYLE: return this.domSanitizer.bypassSecurityTrustStyle(purifiedValue); case SecurityContext.SCRIPT: return this.domSanitizer.bypassSecurityTrustScript(purifiedValue); case SecurityContext.URL: return this.domSanitizer.bypassSecurityTrustUrl(purifiedValue); case SecurityContext.RESOURCE_URL: return this.domSanitizer.bypassSecurityTrustResourceUrl(purifiedValue); default: return null; } } } 

Eso es todo lo que se necesita para proteger la aplicación al insertar HTML en la página. + 7KB gzip de DOMPurify y micropipe. Sin embargo, desde que subimos aquí, intentaremos seguir adelante. La abstracción de las clases significa que Angular sugiere la posibilidad de crear sus propias implementaciones. ¿Por qué crear una tubería y marcar el contenido como seguro si puede usar DOMPurify de inmediato como DomSanitizer ?



DomPurifyDomSanitizer


Cree una clase que herede de DomSanitizer y delegue la eliminación de valores a DOMPurify. Para el futuro, implementaremos de inmediato el servicio Sanitizer y lo utilizaremos tanto en la tubería como en DomSanitizer . Esto nos ayudará en el futuro, ya que aparecerá un único punto de entrada a DOMPurify. La implementación de SafeValue y todo lo relacionado con este concepto es el código privado de Angular, por lo que tendremos que escribirlo nosotros mismos. Sin embargo, como vimos en el código fuente, esto no es difícil.


Vale la pena señalar que, además de HTML, hay otros SecurityContext y, en particular, para la limpieza de estilos DOMPurify como tal no es adecuado. Sin embargo, sus funciones se expanden usando ganchos, más sobre eso más adelante. Además, se puede pasar un objeto de configuración a DOMPurify, y la inyección de dependencia es perfecta para esto. Agregue un token a nuestro código que nos permita proporcionar parámetros para DOMPurify:


 @NgModule({ // ... providers: [ { provide: DOMPURIFY_CONFIG, useValue: {FORBID_ATTR: ['id']}, }, ], // ... }) export class AppModule {} 

 @Injectable({ providedIn: 'root', }) export class NgDompurifySanitizer extends Sanitizer { constructor( @Inject(DOMPURIFY_CONFIG) private readonly config: NgDompurifyConfig, ) { super(); } sanitize( _context: SecurityContext, value: {} | string | null, config: NgDompurifyConfig = this.config, ): string { return sanitize(String(value || ''), config); } } 

DomSanitizer trabajo DomSanitizer con estilos se implementa de tal manera que solo recibe el valor de la regla CSS en la entrada, pero no el nombre del estilo en sí. Por lo tanto, para permitir que nuestro desinfectante borre los estilos, debemos proporcionarle un método que reciba una cadena de valor y devuelva una cadena borrada. Para hacer esto, agregue otro token y defínalo de manera predeterminada como una función que despeje la cadena a nada, ya que no nos haremos responsables de los estilos potencialmente dañinos.


 export const SANITIZE_STYLE = new InjectionToken<SanitizeStyle>( 'A function that sanitizes value for a CSS rule', { factory: () => () => '', providedIn: 'root', }, ); 

  sanitize( context: SecurityContext, value: {} | string | null, config: NgDompurifyConfig = this.config, ): string { return context === SecurityContext.STYLE ? this.sanitizeStyle(String(value)) : sanitize(String(value || ''), config); } 

De esta forma, @HostBinding('style.*') los estilos que están directamente involucrados en el enlace a través de [style.*] O @HostBinding('style.*') . Como recordamos, el comportamiento del desinfectante angular incorporado es eliminar todos los estilos en línea. Honestamente, no sé por qué decidieron hacer esto, ya que escribieron métodos para eliminar reglas CSS sospechosas, pero si necesita implementar, por ejemplo, un editor WYSIWYG, no puede prescindir de estilos en línea. Afortunadamente, DOMPurify le permite agregar ganchos para expandir las capacidades de la biblioteca. La sección de ejemplos incluso tiene código que borra los estilos en los elementos DOM y en todas HTMLStyleElement etiquetas HTMLStyleElement .


Ganchos


Usando DOMPurify.addHook() puede registrar métodos que reciben el elemento actual en una etapa diferente de limpieza. Para conectarlos, agregaremos el tercer y último token. Ya tenemos un método para limpiar estilos, solo necesita llamarlo para todas las reglas en línea. Para hacer esto, registre el gancho requerido en el constructor de nuestro servicio. Repasaremos todos los ganchos que nos llegaron usando DI:


  constructor( @Inject(DOMPURIFY_CONFIG) private readonly config: NgDompurifyConfig, @Inject(SANITIZE_STYLE) private readonly sanitizeStyle: SanitizeStyle, @Inject(DOMPURIFY_HOOKS) hooks: ReadonlyArray<NgDompurifyHook>, ) { super(); addHook('afterSanitizeAttributes', createAfterSanitizeAttributes(this.sanitizeStyle)); hooks.forEach(({name, hook}) => { addHook(name, hook); }); } 

Nuestro servicio está listo y solo queda escribir su DomSanitizer que solo le transferirá el contenido para su limpieza. También debe implementar los métodos bypassSecurityTrust*** , pero ya los vimos en el código fuente angular. Ahora DOMPurify está disponible de manera puntual, utilizando una tubería o servicio, y automáticamente en toda la aplicación.


Conclusión


Descubrimos DomSanitizer en Angular y ya no enmascaramos el problema de insertar HTML arbitrario. Ahora puede insertar mensajes SVG y HTML de forma segura desde el servidor. Para que el desinfectante funcione correctamente, aún debe proporcionarle un método para limpiar los estilos. El CSS malicioso es mucho menos común que otros tipos de ataques, los tiempos de las expresiones CSS han quedado atrás y lo que hagas con CSS depende de ti. Puede escribir un controlador usted mismo, puede escupir y omitir todo lo que no tenga corchetes, seguro. O puede hacer un poco de trampa y echar un vistazo más de cerca a las fuentes de Angular. El método _sanitizeStyle está en el paquete @angular/core , mientras que DomSanitizerImpl está en el @angular/platform-browser y, aunque este método es privado en su paquete, el equipo de Angular no duda en acceder a él por su nombre privado ɵ_sanitizeStyle . Podemos hacer lo mismo y transferir este método a nuestro desinfectante. Por lo tanto, obtenemos el mismo nivel de estilos de limpieza que el predeterminado, pero con la capacidad de usar estilos en línea al insertar código HTML. El nombre de esta importación privada no ha cambiado desde que apareció en la sexta versión, pero debe tener cuidado con estas cosas, nada impide que el equipo Angular la renombre, mueva o elimine en cualquier momento.


El código descrito en el artículo está en Github.
También disponible como paquete npm tinkoff / ng-dompurify:
https://www.npmjs.com/package/@tinkoff/ng-dompurify
Puedes jugar un poco con stackblitz con una demo

Source: https://habr.com/ru/post/459396/


All Articles