Todo mundo que teve que incorporar conteúdo HTML no DOM no Angular viu essa mensagem. Obviamente, todos obtemos o conteúdo verificado em nosso próprio servidor e queremos apenas cobrir a mensagem de erro. Ou incorporamos o HTML de nossas constantes, alinhamos nossos ícones SVG, porque precisamos colori-los na cor do texto. Afinal, nada de ruim acontecerá se dissermos ao Angular - não é um desvio, tudo está limpo lá.
Na maioria das vezes, pode ser assim, mas em grandes projetos com uma massa de desenvolvedores que escrevem componentes independentes, você nunca sabe ao certo onde pode estar o seu código. E se você, como eu, está desenvolvendo uma biblioteca de componentes reutilizáveis, essa situação precisa ser resolvida pela raiz.

Sanitize e DomSanitizer
Angular tem classes abstratas cujas implementações são projetadas para limpar o conteúdo de lixo eletrônico malicioso:
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 }
A classe interna específica que DomSanitizerImpl
Angular usa na criação do DOM. É o seu pessoal que adiciona diretrizes, componentes e tubulações para dizer à estrutura que esse conteúdo pode ser confiável.
O código fonte desta classe é bastante simples:
https://github.com/angular/angular/blob/8.1.0/packages/platform-browser/src/security/dom_sanitization_service.ts#L148
Este serviço apresenta o conceito de SafeValue
. Em essência, essa é apenas uma classe de wrapper para uma string. Quando o Renderer
insere um valor por meio de ligação, seja innerHTML
, @HostBinding
, style
ou src
, o valor é executado pelo método sanitize
. Se o SafeValue
já tiver chegado lá, ele simplesmente retornará a string SafeValue
, caso contrário, ele limpa o conteúdo por conta própria.
Angular não é uma biblioteca especializada em limpeza de códigos de malware. Portanto, a estrutura segue corretamente o caminho de menor risco e corta tudo o que causa preocupação. O código SVG se transforma em linhas vazias, os estilos embutidos são excluídos etc. No entanto, existem bibliotecas projetadas apenas para proteger o DOM, uma das quais é DOMPurify:
https://github.com/cure53/DOMPurify

O tubo SafeHtml correto
Depois de conectar o DOMPurify, podemos criar um canal que não apenas marque o conteúdo como seguro, mas também o limpe. Para fazer isso, DOMPurify.sanitize
valor de entrada através do método DOMPurify.sanitize
e marque-o como seguro no contexto apropriado:
@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; } } }
Isso é o suficiente para proteger o aplicativo ao inserir HTML na página. + 7 KB de gzip do DOMPurify e micropipe. No entanto, desde que escalamos aqui, tentaremos seguir em frente. A abstração das classes significa que o Angular sugere a possibilidade de criar suas próprias implementações. Por que criar um canal e marcar o conteúdo como seguro se você pode usar o DOMPurify imediatamente como DomSanitizer
?

DomPurifyDomSanitizer
Crie uma classe que herda do DomSanitizer
e delega a limpeza de valores para DOMPurify. Para o futuro, implementaremos imediatamente o serviço Sanitizer
e o usaremos no pipe e no DomSanitizer
. Isso nos ajudará no futuro, pois um único ponto de entrada para DOMPurify aparecerá. A implementação do SafeValue
e tudo relacionado a esse conceito é o código privado da Angular, por isso teremos que escrevê-lo. No entanto, como vimos no código-fonte, isso não é difícil.
Vale ressaltar que, além do HTML, existem outros SecurityContext
e, em particular, para a limpeza de estilos DOMPurify, como tal, não é adequado. No entanto, suas funções são expandidas usando ganchos, mais sobre isso posteriormente. Além disso, um objeto de configuração pode ser passado para o DOMPurify, e a injeção de dependência é perfeita para isso. Adicione um token ao nosso código que permita fornecer parâmetros para o DOMPurify:
@NgModule({
@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
trabalho DomSanitizer
com estilos é implementado de forma a receber apenas o valor da regra CSS na entrada, mas não o nome do próprio estilo. Portanto, para permitir que o nosso desinfetante limpe os estilos, precisamos fornecer a ele um método que receberá uma sequência de valores e retornará uma sequência limpa. Para fazer isso, adicione outro token e defina-o por padrão como uma função que limpa a string para nada, pois não assumiremos a responsabilidade por estilos potencialmente prejudiciais.
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); }
Dessa forma, limparemos os estilos diretamente envolvidos na ligação via [style.*]
Ou @HostBinding('style.*')
. Como lembramos, o comportamento do desinfetante Angular interno é remover todos os estilos em linha. Honestamente, não sei por que eles decidiram fazer isso, pois criaram métodos para remover regras CSS suspeitas, mas se você precisar implementar, por exemplo, um editor WYSIWYG, não poderá usar estilos inline. Felizmente, o DOMPurify permite adicionar ganchos para expandir os recursos da biblioteca. A seção de exemplos ainda possui um código que limpa os estilos nos elementos DOM e nas tags HTMLStyleElement
inteiras.
Hooks
Usando DOMPurify.addHook()
você pode registrar métodos que recebem o elemento atual em um estágio diferente da limpeza. Para conectá-los, adicionaremos o terceiro e o último token. Já temos um método para limpar estilos, basta chamá-lo para todas as regras em linha. Para fazer isso, registre o gancho necessário no construtor de nosso serviço. Analisaremos todos os ganchos que chegaram até nós usando o 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); }); }
Nosso serviço está pronto e resta apenas gravar o DomSanitizer
que apenas transfere conteúdo para limpeza. Você também precisa implementar os métodos bypassSecurityTrust***
, mas já os espionamos no código-fonte Angular. Agora, o DOMPurify está disponível no sentido horário, usando um canal ou serviço, e automaticamente em todo o aplicativo.
Conclusão
Descobrimos DomSanitizer
no Angular e não mais mascaramos o problema de inserir HTML arbitrário. Agora você pode incorporar com segurança mensagens SVG e HTML do servidor. Para que o desinfetante funcione corretamente, você ainda precisa fornecer a ele um método para limpar estilos. CSS mal-intencionado é muito menos comum que outros tipos de ataques, os tempos das expressões CSS acabaram e o que você faz com CSS é com você. Você mesmo pode escrever um manipulador, pode cuspir e pular tudo que não tem colchetes, com certeza. Ou você pode trapacear um pouco e dar uma olhada nas fontes do Angular. O método _sanitizeStyle
está no pacote @angular/core
, enquanto o DomSanitizerImpl
está no pacote @angular/platform-browser
e, embora esse método seja privado em seu pacote, a equipe Angular não hesita em acessá-lo por seu nome particular ɵ_sanitizeStyle
. Podemos fazer o mesmo e transferir esse método para o nosso desinfetante. Portanto, obtemos o mesmo nível de estilos de limpeza que o padrão, mas com a capacidade de usar estilos embutidos ao inserir código HTML. O nome dessa importação privada não mudou desde que apareceu na 6ª versão, mas você precisa ter cuidado com isso, nada impede que a equipe Angular renomeie, mova ou exclua a qualquer momento.
O código descrito no artigo está no Github
Também disponível como pacote npm tinkoff / ng-dompurify:
https://www.npmjs.com/package/@tinkoff/ng-dompurify
Você pode jogar um pouco com o stackblitz com uma demo