Skip to content

Commit

Permalink
feat(ngdart): bring back DomSanitizationService
Browse files Browse the repository at this point in the history
Signed-off-by: Gavin Zhao <git@gzgz.dev>
  • Loading branch information
GZGavinZhao committed Mar 14, 2023
1 parent 2e33802 commit f1b6572
Show file tree
Hide file tree
Showing 4 changed files with 248 additions and 33 deletions.
85 changes: 62 additions & 23 deletions _tests/test/core/linker/security_integration_test.dart
Original file line number Diff line number Diff line change
@@ -1,61 +1,77 @@
@TestOn('browser')

import 'dart:html';

import 'package:ngtest/angular_test.dart';
import 'package:ngdart/src/security/dom_sanitization_service.dart';
import 'package:test/test.dart';
import 'package:ngdart/angular.dart';
import 'package:ngtest/angular_test.dart';

import 'security_integration_test.template.dart' as ng;

void main() {
tearDown(disposeAnyRunningTest);

test('should escape unsafe attributes', () async {
final testBed = NgTestBed<UnsafeAttributeComponent>(
ng.createUnsafeAttributeComponentFactory());
const unsafeUrl = 'javascript:alert(1)';
final testBed = NgTestBed(ng.createUnsafeAttributeComponentFactory());
final testFixture = await testBed.create();
final a = testFixture.rootElement.querySelector('a') as AnchorElement;
expect(a.href, matches(r'.*/hello$'));
await testFixture.update((component) {
component.href = 'javascript:alert(1)';
component.href = unsafeUrl;
});
expect(a.href, isNot(contains('javascript')));
}, tags: 'fails-on-ci');
expect(a.href, equals('unsafe:$unsafeUrl'));
});

test('should not escape values marked as trusted', () async {
final testBed = NgTestBed(ng.createTrustedValueComponentFactory());
final testFixture = await testBed.create();
final a = testFixture.rootElement.querySelector('a') as AnchorElement;
expect(a.href, 'javascript:alert(1)');
});

test('should throw error when using the wrong trusted value', () async {
final testBed = NgTestBed(ng.createWrongTrustedValueComponentFactory());
expect(testBed.create(), throwsA(isUnsupportedError));
});

test('should escape unsafe styles', () async {
final testBed =
NgTestBed<UnsafeStyleComponent>(ng.createUnsafeStyleComponentFactory());
final testBed = NgTestBed(ng.createUnsafeStyleComponentFactory());
final testFixture = await testBed.create();
final div = testFixture.rootElement.querySelector('div')!;
expect(div.style.background, matches('red'));
final div = testFixture.rootElement.querySelector('div');
expect(div?.style.background, matches('red'));
await testFixture.update((component) {
component.backgroundStyle = 'url(javascript:evil())';
});
expect(div.style.background, isNot(contains('javascript')));
expect(div?.style.background, isNot(contains('javascript')));
});

test('should escape unsafe HTML', () async {
final testBed =
NgTestBed<UnsafeHtmlComponent>(ng.createUnsafeHtmlComponentFactory());
final testBed = NgTestBed(ng.createUnsafeHtmlComponentFactory());
final testFixture = await testBed.create();
final div = testFixture.rootElement.querySelector('div')!;
expect(div.innerHtml, 'some <p>text</p>');
final div = testFixture.rootElement.querySelector('div');
expect(div?.innerHtml, 'some <p>text</p>');
await testFixture.update((component) {
component.html = 'ha <script>evil()</script>';
var c = component;
c.html = 'ha <script>evil()</script>';
});
expect(div.innerHtml, 'ha ');
expect(div?.innerHtml, 'ha ');
await testFixture.update((component) {
component.html = 'also <img src="x" onerror="evil()"> evil';
var c = component;
c.html = 'also <img src="x" onerror="evil()"> evil';
});
expect(div.innerHtml, 'also <img src="x"> evil');
expect(div?.innerHtml, 'also <img src="x"> evil');
await testFixture.update((component) {
final srcdoc = '<div></div><script></script>';
component.html = 'also <iframe srcdoc="$srcdoc"> content</iframe>';
var c = component;
c.html = 'also <iframe srcdoc="$srcdoc"> content</iframe>';
});
expect(
div.innerHtml,
'also ',
div?.innerHtml,
'also <iframe> content</iframe>',
);
}, tags: 'fails-on-ci');
});
}

@Component(
Expand All @@ -66,6 +82,29 @@ class UnsafeAttributeComponent {
String href = 'hello';
}

@Component(
selector: 'trusted-value',
template: '<a [href]="href">Link Title</a>',
providers: [ClassProvider(DomSanitizationService)])
class TrustedValueComponent {
SafeUrl href;

TrustedValueComponent(DomSanitizationService sanitizer)
: href = sanitizer.bypassSecurityTrustUrl('javascript:alert(1)');
}

@Component(
selector: 'wrong-trusted-value',
template: '<a [href]="href">Link Title</a>',
providers: [ClassProvider(DomSanitizationService)])
class WrongTrustedValueComponent {
late SafeHtml href;

WrongTrustedValueComponent(DomSanitizationService sanitizer) {
href = sanitizer.bypassSecurityTrustHtml('javascript:alert(1)');
}
}

@Component(
selector: 'unsafe-style',
template: '<div [style.background]="backgroundStyle"></div>',
Expand Down
168 changes: 168 additions & 0 deletions ngdart/lib/src/security/dom_sanitization_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import 'package:ngdart/di.dart' show Injectable;

import 'package:ngdart/src/utilities.dart';
import 'html_sanitizer.dart';
import 'style_sanitizer.dart';
import 'url_sanitizer.dart';
import 'sanitization_service.dart';

abstract class SafeValue {}

abstract class SafeHtml extends SafeValue {}

abstract class SafeStyle extends SafeValue {}

abstract class SafeUrl extends SafeValue {}

abstract class SafeResourceUrl extends SafeValue {}

/// DomSanitizationService helps preventing Cross Site Scripting Security bugs
/// (XSS) by sanitizing values to be safe to use in the different DOM contexts.
///
/// For example, when binding a URL in an `<a [href]="someUrl">` hyperlink,
/// _someUrl_ will be sanitized so that an attacker cannot inject a
/// `javascript:` URL that would execute code on the website.
///
/// In specific situations, it might be necessary to disable sanitization, for
/// example if the application genuinely needs to produce a javascript:
/// style link with a dynamic value in it.
///
/// Users can bypass security by constructing a value with one of the
/// `bypassSecurityTrust...` methods, and then binding to that value from the
/// template.
///
/// These situations should be very rare, and extraordinary care must be taken
/// to avoid creating a Cross Site Scripting (XSS) security bug!
///
/// When using `bypassSecurityTrust...`, make sure to call the method as
/// early as possible and as close as possible to the source of the value,
/// to make it easy to verify that no security bug is created by its use.
///
/// It is not required (and not recommended) to bypass security if the value
/// is safe, for example, a URL that does not start with a suspicious protocol, or an
/// HTML snippet that does not contain dangerous code. The sanitizer leaves
/// safe values intact.
@Injectable()
class DomSanitizationService implements SanitizationService {
static const _instance = DomSanitizationService._();

// Force a global static singleton across DDC instances for this service. In
// angular currently it is already a single instance across all instances for
// performance reasons. This allows a check to occur that this is really the
// same sanitizer is used.
factory DomSanitizationService() => _instance;

// Const to enforce statelessness.
const DomSanitizationService._();

@override
String? sanitizeHtml(value) {
if (value == null) return null;
if (value is SafeHtmlImpl) return value.changingThisWillBypassSecurityTrust;
if (value is SafeValue) {
throw UnsupportedError(
'Unexpected SecurityContext $value, expecting html');
}
return sanitizeHtmlInternal(unsafeCast(value));
}

@override
String? sanitizeStyle(value) {
if (value == null) return null;
if (value is SafeStyleImpl) {
return value.changingThisWillBypassSecurityTrust;
}
if (value is SafeValue) {
throw UnsupportedError('Unexpected SecurityContext $value, '
'expecting style');
}
if (value == null) return null;
return internalSanitizeStyle(value is String ? value : value.toString());
}

@override
String? sanitizeUrl(value) {
if (value == null) return null;
if (value is SafeUrlImpl) return value.changingThisWillBypassSecurityTrust;
if (value is SafeValue) {
throw UnsupportedError('Unexpected SecurityContext $value, '
'expecting url');
}
return internalSanitizeUrl(value.toString());
}

@override
String? sanitizeResourceUrl(value) {
if (value == null) return null;
if (value is SafeResourceUrlImpl) {
return value.changingThisWillBypassSecurityTrust;
}
if (value is SafeValue) {
throw UnsupportedError('Unexpected SecurityContext $value, '
'expecting resource url');
}
throw UnsupportedError(
'Security violation in resource url. Create SafeValue');
}

/// Bypass security and trust the given value to be safe HTML.
///
/// Only use this when the bound HTML is unsafe (e.g. contains `<script>`
/// tags) and the code should be executed. The sanitizer will leave safe HTML
/// intact, so in most situations this method should not be used.
///
/// WARNING: calling this method with untrusted user data will cause severe
/// security bugs!
SafeHtml bypassSecurityTrustHtml(String? value) => SafeHtmlImpl(value ?? '');

/// Bypass security and trust the given value to be safe style value (CSS).
///
/// WARNING: calling this method with untrusted user data will cause severe
/// security bugs!
SafeStyle bypassSecurityTrustStyle(String? value) =>
SafeStyleImpl(value ?? '');

/// Bypass security and trust the given value to be a safe style URL, i.e. a
/// value that can be used in hyperlinks or `<iframe src>`.
///
/// WARNING: calling this method with untrusted user data will cause severe
/// security bugs!
SafeUrl bypassSecurityTrustUrl(String? value) => SafeUrlImpl(value ?? '');

/// Bypass security and trust the given value to be a safe resource URL, i.e.
/// a location that may be used to load executable code from, like
/// <script src>.
///
/// WARNING: calling this method with untrusted user data will cause severe
/// security bugs!
SafeResourceUrl bypassSecurityTrustResourceUrl(String? value) =>
SafeResourceUrlImpl(value ?? '');
}

abstract class SafeValueImpl implements SafeValue {
/// Named this way to allow security teams to
/// to search for BypassSecurityTrust across code base.
final String changingThisWillBypassSecurityTrust;
SafeValueImpl(this.changingThisWillBypassSecurityTrust);

@override
String toString() => changingThisWillBypassSecurityTrust;
}

class SafeHtmlImpl extends SafeValueImpl implements SafeHtml {
SafeHtmlImpl(String value) : super(value);
}

class SafeStyleImpl extends SafeValueImpl implements SafeStyle {
SafeStyleImpl(String value) : super(value);
}

class SafeUrlImpl extends SafeValueImpl implements SafeUrl {
SafeUrlImpl(String value) : super(value) {
print('SafeUrlImpl: value passed is $value');
}
}

class SafeResourceUrlImpl extends SafeValueImpl implements SafeResourceUrl {
SafeResourceUrlImpl(String value) : super(value);
}
16 changes: 6 additions & 10 deletions ngdart/lib/src/security/safe_html_adapter.dart
Original file line number Diff line number Diff line change
@@ -1,41 +1,37 @@
/// The top-level methods are intended to be used only by code generated by the
/// compiler when it "sees" that a potential unsafe operation would otherwise be
/// used (i.e. `<div [innerHtml]="someValue"></div>`).
import 'html_sanitizer.dart';
import 'style_sanitizer.dart';
import 'url_sanitizer.dart';
import 'dom_sanitization_service.dart';

/// Converts [stringOrSafeOrBypass] into a `String` safe to use within the DOM.
String? sanitizeHtml(Object? stringOrSafeOrBypass) {
if (stringOrSafeOrBypass == null) {
return null;
}
final unsafeString = stringOrSafeOrBypass.toString();
return sanitizeHtmlInternal(unsafeString);
return DomSanitizationService().sanitizeHtml(stringOrSafeOrBypass);
}

/// Converts [stringOrSafeOrBypass] into a `String` safe to use within the DOM.
String? sanitizeStyle(Object? stringOrSafeOrBypass) {
if (stringOrSafeOrBypass == null) {
return null;
}
final unsafeString = stringOrSafeOrBypass.toString();
return internalSanitizeStyle(unsafeString);
return DomSanitizationService().sanitizeStyle(stringOrSafeOrBypass);
}

/// Converts [stringOrSafeOrBypass] into a `String` safe to use within the DOM.
String? sanitizeUrl(Object? stringOrSafeOrBypass) {
if (stringOrSafeOrBypass == null) {
return null;
}
final unsafeString = stringOrSafeOrBypass.toString();
return internalSanitizeUrl(unsafeString);
return DomSanitizationService().sanitizeUrl(stringOrSafeOrBypass);
}

/// Converts [stringOrSafeOrBypass] into a `String` safe to use within the DOM.
String? sanitizeResourceUrl(Object? stringOrSafeOrBypass) {
if (stringOrSafeOrBypass == null) {
return null;
}
return stringOrSafeOrBypass.toString();
return DomSanitizationService().sanitizeResourceUrl(stringOrSafeOrBypass);
}
12 changes: 12 additions & 0 deletions ngdart/lib/src/security/sanitization_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/// [SanitizationService] is used by the views to sanitize values as create
/// SafeValue equivalents that can be used to bind to in templates.
abstract class SanitizationService {
// Sanitizes html content.
String? sanitizeHtml(value);
// Sanitizes css style.
String? sanitizeStyle(value);
// Sanitizes url link.
String? sanitizeUrl(value);
// Sanitizes resource loading url.
String? sanitizeResourceUrl(value);
}

0 comments on commit f1b6572

Please sign in to comment.