Skip to content

Commit

Permalink
feat(css): support sanitizing CSS through provided handler
Browse files Browse the repository at this point in the history
  • Loading branch information
waterplea authored Aug 29, 2019
2 parents 7ef00ab + 3f99451 commit be0d3a6
Show file tree
Hide file tree
Showing 31 changed files with 523 additions and 119 deletions.
115 changes: 104 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
[![npm version](https://img.shields.io/npm/v/@tinkoff/ng-dompurify.svg?style=flat-square)](https://npmjs.com/package/@tinkoff/ng-dompurify)
[![code style: @tinkoff/linters](https://img.shields.io/badge/code%20style-%40tinkoff%2Flinters-blue?style=flat-square)](https://github.com/TinkoffCreditSystems/linters)

> This library implements `DOMPurify` as Angular entire `DomSanitizer` and as standalone Sanitizer or Pipe.
> It delegates sanitizing to `DOMPurify` and supports the same configuration. See [DOMPurify](https://github.com/cure53/DOMPurify).
> This library implements `DOMPurify` as Angular entire `DomSanitizer` and as
standalone `Sanitizer` or `Pipe`. It delegates sanitizing to `DOMPurify` and
supports the same configuration. See [DOMPurify](https://github.com/cure53/DOMPurify).

## Install

Expand All @@ -16,14 +17,15 @@ $ npm install @tinkoff/ng-dompurify

## How to use

Either use pipe to sanitize your content when binding to `[innerHTML]` or use `NgDompurifySanitizer` service manually.
Either use pipe to sanitize your content when binding to `[innerHTML]`
or use `NgDompurifySanitizer` service manually.

You can also substitute entire Angular `DomSanitizer` with `DOMPurify`:

```typescript
import {BrowserModule, DomSanitizer} from '@angular/platform-browser';
import {DomSanitizer} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {NgDompurifyDomSanitizer} from 'ng-dompurify';
import {NgDompurifyDomSanitizer} from '@tinkoff/ng-dompurify';
// ...

@NgModule({
Expand All @@ -39,15 +41,106 @@ import {NgDompurifyDomSanitizer} from 'ng-dompurify';
export class AppModule {}
```

Config for `NgDompurifySanitizer` or `NgDompurifyDomSanitizer` can be provided using token `DOMPURIFY_CONFIG`.
## Configuring

_Note: Keep in mind that binding to `[style]` would be disabled if you do it this way, since `DOMPurify` does not support sanitizing CSS. By default inline CSS is also stripped like when using Angular default sanitizer._
`NgDompurifyPipe` supports passing DOMPurify config as an argument.
Config for `NgDompurifySanitizer` or `NgDompurifyDomSanitizer` can be
provided using token `DOMPURIFY_CONFIG`:

```typescript
import {DomSanitizer} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {NgDompurifyDomSanitizer, DOMPURIFY_CONFIG} from '@tinkoff/ng-dompurify';
// ...

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

## CSS sanitization

DOMPurify does not support sanitizing CSS. `DomSanitizer` in Angular
is organized in such a way that it only received CSS rule value, and
not the name. Therefore, a method taking in CSS rule value and returning
a sanitized value is required to support CSS. You can try using internal
Angular import `ɵ_sanitizeStyle` since they use it themselves to use it in
`platform-browser` package where `DomSanitizer` is implemented. This way
level of CSS sanitization will be equal to native Angular with added benefit
of supporting inline styles in `[innerHTML]` bindings.

```typescript
import {DomSanitizer} from '@angular/platform-browser';
import {NgModule, ɵ_sanitizeStyle} from '@angular/core';
import {NgDompurifyDomSanitizer, SANITIZE_STYLE} from '@tinkoff/ng-dompurify';

@NgModule({
// ...
providers: [
{
provide: DomSanitizer,
useClass: NgDompurifyDomSanitizer,
},
{
provide: SANITIZE_STYLE,
useValue: ɵ_sanitizeStyle,
},
],
// ...
})
export class AppModule {}

```

## Hooks

DOMPurify supports various hooks. You can provide them using `DOMPURIFY_HOOKS` token:

```typescript
import {DomSanitizer} from '@angular/platform-browser';
import {NgModule, ɵ_sanitizeStyle} from '@angular/core';
import {NgDompurifyDomSanitizer, DOMPURIFY_HOOKS, SANITIZE_STYLE} from '@tinkoff/ng-dompurify';

@NgModule({
// ...
providers: [
{
provide: DomSanitizer,
useClass: NgDompurifyDomSanitizer,
},
{
provide: SANITIZE_STYLE,
useValue: ɵ_sanitizeStyle,
},
{
provide: DOMPURIFY_HOOKS,
useValue: [{
name: 'beforeSanitizeAttributes',
hook: (node: Element) => {
node.removeAttribute('id');
}
}],
}
],
// ...
})
export class AppModule {}

```

## Demo

You can see live demo here:
https://stackblitz.com/edit/ng-dompurify-demo

## Known issues

`DOMPurify` does not support sanitizing CSS but it supports adding hooks through which you can sanitize it yourself. Adding hooks is not yet supported by `NgDompurify`.
5 changes: 5 additions & 0 deletions projects/demo/src/app/app.component.html
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
<p>
In these examples <code>ng-dompurify</code> is configured to use
<code>ɵ_sanitizeStyle</code> from <code>'@angular/core'</code> as
<code>SANITIZE_STYLE</code>.
</p>
<app-pipe-example></app-pipe-example>
<app-sanitizer-example></app-sanitizer-example>
8 changes: 6 additions & 2 deletions projects/demo/src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {BrowserModule, DomSanitizer} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {NgModule, ɵ_sanitizeStyle} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {NgDompurifyDomSanitizer, NgDompurifyModule} from '@tinkoff/ng-dompurify';
import {NgDompurifyDomSanitizer, NgDompurifyModule, SANITIZE_STYLE} from '@tinkoff/ng-dompurify';
import {AppComponent} from './app.component';
import {PipeExampleComponent} from './pipe-example/pipe-example.component';
import {SanitizerExampleComponent} from './sanitizer-example/sanitizer-example.component';
Expand All @@ -14,6 +14,10 @@ import {SanitizerExampleComponent} from './sanitizer-example/sanitizer-example.c
provide: DomSanitizer,
useClass: NgDompurifyDomSanitizer,
},
{
provide: SANITIZE_STYLE,
useValue: ɵ_sanitizeStyle,
},
],
bootstrap: [AppComponent],
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Component, Inject, SecurityContext} from '@angular/core';
import {DomSanitizer, SafeValue} from '@angular/platform-browser';

const dirtyHtml = `<p>HELLO<iframe/\/src=JavaScript:alert&lpar;1)></ifrAMe><br>goodbye</p>`;
const dirtyHtml = `<p style="color: red; background: expression(evil)"> HELLO <iframe/\/src=JavaScript:alert&lpar;1)></ifrAMe><br>goodbye</p>`;

@Component({
selector: 'app-pipe-example',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Component} from '@angular/core';

const svg = `<svg width="56" height="56">
<path fill="#D75A4A" style="malicious" d="M24.85,10.126c2.018-4.783,6.628-8.125,11.99-8.125c7.223,0,12.425,6.179,13.079,13.543 c0,0,0.353,1.828-0.424,5.119c-1.058,4.482-3.545,8.464-6.898,11.503L24.85,48L7.402,32.165c-3.353-3.038-5.84-7.021-6.898-11.503 c-0.777-3.291-0.424-5.119-0.424-5.119C0.734,8.179,5.936,2,13.159,2C18.522,2,22.832,5.343,24.85,10.126z"/>
<path fill="#D75A4A" d="M24.85,10.126c2.018-4.783,6.628-8.125,11.99-8.125c7.223,0,12.425,6.179,13.079,13.543 c0,0,0.353,1.828-0.424,5.119c-1.058,4.482-3.545,8.464-6.898,11.503L24.85,48L7.402,32.165c-3.353-3.038-5.84-7.021-6.898-11.503 c-0.777-3.291-0.424-5.119-0.424-5.119C0.734,8.179,5.936,2,13.159,2C18.522,2,22.832,5.343,24.85,10.126z"/>
</svg>`;

@Component({
Expand Down
2 changes: 1 addition & 1 deletion projects/demo/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ body {
textarea {
display: block;
width: 100%;
height: 200px;
height: 160px;
resize: none;
border-radius: 4px;
padding: 12px 16px;
Expand Down
3 changes: 2 additions & 1 deletion projects/ng-dompurify/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
"name": "@tinkoff/ng-dompurify",
"version": "1.0.0",
"peerDependencies": {
"@angular/core": ">=4.0.0"
"@angular/core": ">=4.0.0",
"@angular/platform-browser": ">=4.0.0"
},
"description": "Inclusive Angular API for DOMPurify",
"keywords": ["angular", "ng", "dompurify", "sanitizer", "DomSanitizer", "tinkoff"],
Expand Down
4 changes: 0 additions & 4 deletions projects/ng-dompurify/src/lib/const/default-config.ts

This file was deleted.

13 changes: 0 additions & 13 deletions projects/ng-dompurify/src/lib/const/dompurify-config.ts

This file was deleted.

7 changes: 1 addition & 6 deletions projects/ng-dompurify/src/lib/ng-dompurify-dom.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,10 @@ import {SafeScriptImplementation} from './safe-value/safe-script-implementation'
import {SafeUrlImplementation} from './safe-value/safe-url-implementation';
import {SafeResourceUrlImplementation} from './safe-value/safe-resource-url-implementation';
import {AbstractSafeValue} from './safe-value/absctract-safe-value';
import {DOMPURIFY_CONFIG} from './const/dompurify-config';
import {NgDompurifyConfig} from './types/ng-dompurify-config';
import {NgDompurifySanitizer} from './ng-dompurify.service';

/**
* Implementation of Angular {@link DomSanitizer} purifying via dompurify and {@link NgDompurifySanitizer}
*
* use {@link DOMPURIFY_CONFIG} token to provide config ({@link NgDompurifyConfig})
* Implementation of Angular {@link DomSanitizer} purifying via DOMPurify and {@link NgDompurifySanitizer}
*/
@Injectable()
export class NgDompurifyDomSanitizer extends DomSanitizer {
Expand All @@ -33,7 +29,6 @@ export class NgDompurifyDomSanitizer extends DomSanitizer {
switch (context) {
case SecurityContext.SCRIPT:
case SecurityContext.STYLE:
throw new Error('dompurify supports HTML, URL and RESOURSE_URL contexts');
case SecurityContext.HTML:
case SecurityContext.URL:
case SecurityContext.RESOURCE_URL:
Expand Down
7 changes: 2 additions & 5 deletions projects/ng-dompurify/src/lib/ng-dompurify.pipe.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import {Pipe, PipeTransform, SecurityContext} from '@angular/core';
import {DomSanitizer, SafeValue} from '@angular/platform-browser';
import {NgDompurifyConfig} from './types/ng-dompurify-config';
import {NGDOMPURIFY_DEFAULT_CONFIG} from './const/default-config';
import {NgDompurifySanitizer} from './ng-dompurify.service';

/**
* Pipe that transforms dirty HTML to clean via {@link NgDompurifySanitizer}
* Pipe that transforms dirty content to clean via {@link NgDompurifySanitizer}
*/
@Pipe({name: 'dompurify'})
export class NgDompurifyPipe implements PipeTransform {
Expand All @@ -17,7 +16,7 @@ export class NgDompurifyPipe implements PipeTransform {
transform(
value: {} | string | null,
context: SecurityContext = SecurityContext.HTML,
config: NgDompurifyConfig = NGDOMPURIFY_DEFAULT_CONFIG,
config: NgDompurifyConfig = {},
): SafeValue | null {
const sanitizedValue = this.sanitizer.sanitize(context, value, config);

Expand All @@ -33,8 +32,6 @@ export class NgDompurifyPipe implements PipeTransform {
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:
Expand Down
40 changes: 33 additions & 7 deletions projects/ng-dompurify/src/lib/ng-dompurify.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import {Injectable, SecurityContext, Sanitizer, Inject} from '@angular/core';
import {DOMPURIFY_CONFIG} from './const/dompurify-config';
import {Inject, Injectable, Sanitizer, SecurityContext} from '@angular/core';
import {sanitize, addHook, removeAllHooks} from 'dompurify';
import {SANITIZE_STYLE} from './tokens/sanitize-style';
import {DOMPURIFY_HOOKS} from './tokens/dompurify-hooks';
import {DOMPURIFY_CONFIG} from './tokens/dompurify-config';
import {NgDompurifyConfig} from './types/ng-dompurify-config';
import {sanitize} from 'dompurify';
import {SanitizeStyle} from './types/sanitize-style';
import {NgDompurifyHook} from './types/ng-dompurify-hook';
import {createUponSanitizeElementHook} from './utils/createUponSanitizeElementHook';
import {createAfterSanitizeAttributes} from './utils/createAfterSanitizeAttributes';

/**
* Implementation of Angular {@link Sanitizer} purifying via dompurify
* Implementation of Angular {@link Sanitizer} purifying via DOMPurify
*
* use {@link DOMPURIFY_CONFIG} token to provide config ({@link NgDompurifyConfig})
* use {@link SANITIZE_STYLE} token to provide a style sanitizing method ({@link SanitizeStyle})
* use {@link DOMPURIFY_HOOKS} token to provide a hooks for DOMPurify ({@link addHook})
*
* Ambient type cannot be used without @dynamic https://github.com/angular/angular/issues/23395
* @dynamic
*/
@Injectable({
providedIn: 'root',
Expand All @@ -15,17 +26,32 @@ export class NgDompurifySanitizer extends Sanitizer {
constructor(
@Inject(DOMPURIFY_CONFIG)
private readonly config: NgDompurifyConfig,
@Inject(SANITIZE_STYLE)
private readonly sanitizeStyle: SanitizeStyle,
@Inject(DOMPURIFY_HOOKS)
hooks: ReadonlyArray<NgDompurifyHook>,
) {
super();

addHook('uponSanitizeElement', createUponSanitizeElementHook(this.sanitizeStyle));
addHook('afterSanitizeAttributes', createAfterSanitizeAttributes(this.sanitizeStyle));

// TODO: a single point of entrance to attach hooks to DOMPurify
hooks.forEach(({name, hook}) => {
addHook(name, hook);
});
}

sanitize(
_: SecurityContext,
context: SecurityContext,
value: {} | string | null,
config: NgDompurifyConfig = this.config,
): string {
return sanitize(String(value || ''), config);
if (context === SecurityContext.SCRIPT) {
throw new Error('DOMPurify does not support SCRIPT context');
}

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

0 comments on commit be0d3a6

Please sign in to comment.