每个不得不在Angular的DOM中嵌入HTML内容的人都看到了此消息。 当然,我们所有人都从我们自己的服务器获取检查的内容,只想覆盖错误消息。 或者我们从常量中嵌入HTML,内联SVG图标,因为我们只需要以文本的颜色为其着色。 毕竟,如果我们只对Angular说的话,就不会有任何不好的事情发生-不会发生漂移,那里的一切都很干净。
多数情况下可能是这样,但是在拥有大量编写独立组件的开发人员的大型项目中,您永远无法确定代码的位置。 而且,如果您像我一样,正在开发可重用组件的库,那么这种情况就需要立即解决。

消毒和DomSanitizer
Angular具有抽象类,这些抽象类的实现旨在清除恶意垃圾的内容:
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 }
特定的内部类DomSanitizerImpl
Angular在构建DOM中使用。 正是他的员工向指令,组件和管道添加了内容,以告诉框架可以信任此内容。
此类的源代码非常简单:
https://github.com/angular/angular/blob/8.1.0/packages/platform-browser/src/security/dom_sanitization_service.ts#L148
该服务引入了SafeValue
的概念。 本质上,这只是一个字符串的包装器类。 当Renderer
通过绑定插入值时,无论是innerHTML
, @HostBinding
, style
还是src
,该值都通过sanitize
方法运行。 如果SafeValue
已经到达那里,则它仅返回包装的字符串,否则它将自行清除内容。
Angular不是专门的恶意软件代码清除库。 因此,该框架非常正确地遵循了风险最小的道路,并消除了引起关注的所有问题。 SVG代码变成空行,删除了内联样式,等等。 但是,有些库只是为了保护DOM而设计的,其中之一就是DOMPurify:
https://github.com/cure53/DOMPurify

正确的SafeHtml管道
连接DOMPurify后,我们可以制作一个不仅将内容标记为安全的管道,还可以对其进行清洁。 为此,请通过DOMPurify.sanitize
方法DOMPurify.sanitize
输入值,然后在适当的上下文DOMPurify.sanitize
其标记为安全的:
@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; } } }
将HTML插入页面时,这就是保护应用程序安全的全部方法。 + 7KB来自DOMPurify和micropipe的gzip。 但是,自从我们爬到这里以来,我们将继续前进。 类的抽象性意味着Angular建议创建自己的实现的可能性。 如果可以立即使用DOMPurify作为DomSanitizer
为什么还要创建管道并将内容标记为安全?

DomPurifyDomSanitizer
创建一个从DomSanitizer
继承的类,并将值的清除委托给DOMPurify。 为了将来,我们将立即实施Sanitizer
服务,并将在管道和DomSanitizer
使用它。 这将对我们将来有帮助,因为将出现DOMPurify的单个入口点。 SafeValue
的实现以及与此概念相关的所有内容都是Angular的私有代码,因此我们将不得不自己编写它。 但是,正如我们在源代码中看到的那样,这并不困难。
值得注意的是,除了HTML外,还有其他SecurityContext
,尤其是对于清洁样式DOMPurify而言是不合适的。 但是,它的功能是使用钩子扩展的,稍后再介绍。 另外,可以将配置对象传递给DOMPurify,并且依赖注入对此非常理想。 在我们的代码中添加一个令牌,该令牌使我们能够为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
处理样式的方式是,仅在输入时接收CSS规则的值,而不接收样式本身的名称。 因此,为了允许我们的清理程序清除样式,我们需要向他提供一种方法,该方法将接收值字符串并返回清除的字符串。 为此,添加另一个标记并将其默认定义为将字符串清除为零的函数,因为我们将不对可能有害的样式负责。
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); }
这样,我们将通过[style.*]
或@HostBinding('style.*')
清除与绑定直接相关@HostBinding('style.*')
。 我们记得,内置的Angular消毒器的行为是删除所有内联样式。 老实说,我不知道他们为什么决定这样做,因为他们编写了删除可疑CSS规则的方法,但是如果您需要实现WYSIWYG编辑器,那么就不能没有内联样式。 幸运的是,DOMPurify允许您添加钩子以扩展库的功能。 示例部分甚至包含清除DOM元素和整个HTMLStyleElement
标签上的HTMLStyleElement
代码。
钩子
使用DOMPurify.addHook()
可以注册在不同清洗阶段接收当前元素的方法。 为了连接它们,我们将添加第三个也是最后一个令牌。 我们已经有一种清理样式的方法,您只需为所有内联规则调用它即可。 为此,请在我们的服务的构造函数中注册所需的钩子。 我们将详细介绍使用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); }); }
我们的服务已准备就绪,仅需编写您的DomSanitizer
,该DomSanitizer
只会将内容传送到其中进行清洁。 您还需要实现bypassSecurityTrust***
方法,但是我们已经在Angular源代码中对其进行了监视。 现在,DOMPurify既可以使用管道或服务逐点使用,也可以在整个应用程序中自动使用。
结论
我们确定了DomSanitizer
在Angular中DomSanitizer
,并且不再掩盖插入任意HTML的问题。 现在,您可以安全地从服务器内联SVG和HTML消息。 为了使清洁剂正常工作,您仍然需要向他提供清洁样式的方法。 恶意CSS比其他类型的攻击要少得多,CSS表达式的时代早已过去,您对CSS的处理取决于您自己。 您可以自己编写一个处理程序,可以肯定地吐出并跳过所有没有括号的内容。 或者,您可以作弊一点,然后仔细看看Angular的来源。 _sanitizeStyle
方法位于@angular/core
软件包中,而DomSanitizerImpl
位于@angular/platform-browser
软件包中,尽管此方法在其软件包中是私有的,但Angular团队会毫不犹豫地使用其私有名称ɵ_sanitizeStyle
进行访问。 我们可以做同样的事情,并将这种方法转移到我们的消毒剂中。 因此,我们可以获得与默认样式相同的清洗样式,但是具有在插入HTML代码时使用内联样式的能力。 自从第6版发布以来,此私人导入的名称就没有更改,但是您需要注意这些事情,没有什么可以阻止Angular团队随时重命名,移动或删除它。
本文中描述的代码在Github上
也可作为npm tinkoff / ng-dompurify软件包提供:
https://www.npmjs.com/package/@tinkoff/ng-dompurify
您可以通过演示与stackblitz一起玩