Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit dae6947

Browse files
committed
feat(ngBindHtml, sce): combine ng-bind-html and ng-bind-html-unsafe
Changes: - remove ng-bind-html-unsafe - ng-bind-html is now in core - ng-bind-html is secure - supports SCE - so you can bind to an arbitrary trusted string - automatic sanitization if $sanitize is available BREAKING CHANGE: ng-html-bind-unsafe has been removed and replaced by ng-html-bind (which has been removed from ngSanitize.) ng-bind-html provides ng-html-bind-unsafe like behavior (innerHTML's the result without sanitization) when bound to the result of $sce.trustAsHtml(string). When bound to a plain string, the string is sanitized via $sanitize before being innerHTML'd. If $sanitize isn't available, it's logs an exception.
1 parent bea9422 commit dae6947

File tree

9 files changed

+134
-149
lines changed

9 files changed

+134
-149
lines changed

Gruntfile.js

-1
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,6 @@ module.exports = function(grunt) {
153153
dest: 'build/angular-sanitize.js',
154154
src: util.wrap([
155155
'src/ngSanitize/sanitize.js',
156-
'src/ngSanitize/directive/ngBindHtml.js',
157156
'src/ngSanitize/filter/linky.js'
158157
], 'module')
159158
},

angularFiles.js

-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ angularFiles = {
7373
'src/ngRoute/routeParams.js',
7474
'src/ngRoute/directive/ngView.js',
7575
'src/ngSanitize/sanitize.js',
76-
'src/ngSanitize/directive/ngBindHtml.js',
7776
'src/ngSanitize/filter/linky.js',
7877
'src/ngMock/angular-mocks.js',
7978
'src/ngMobile/mobile.js',

src/AngularPublic.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ function publishExternalAPI(angular){
7272
style: styleDirective,
7373
option: optionDirective,
7474
ngBind: ngBindDirective,
75-
ngBindHtmlUnsafe: ngBindHtmlUnsafeDirective,
75+
ngBindHtml: ngBindHtmlDirective,
7676
ngBindTemplate: ngBindTemplateDirective,
7777
ngClass: ngClassDirective,
7878
ngClassEven: ngClassEvenDirective,

src/ng/directive/ngBind.js

+13-9
Original file line numberDiff line numberDiff line change
@@ -116,23 +116,27 @@ var ngBindTemplateDirective = ['$interpolate', function($interpolate) {
116116

117117
/**
118118
* @ngdoc directive
119-
* @name ng.directive:ngBindHtmlUnsafe
119+
* @name ng.directive:ngBindHtml
120120
*
121121
* @description
122122
* Creates a binding that will innerHTML the result of evaluating the `expression` into the current
123-
* element. *The innerHTML-ed content will not be sanitized!* You should use this directive only if
124-
* {@link ngSanitize.directive:ngBindHtml ngBindHtml} directive is too
125-
* restrictive and when you absolutely trust the source of the content you are binding to.
123+
* element in a secure way. By default, the innerHTML-ed content will be sanitized using the {@link
124+
* ngSanitize.$sanitize $sanitize} service. To utilize this functionality, ensure that `$sanitize`
125+
* is available, for example, by including {@link ngSanitize} in your module's dependencies (not in
126+
* core Angular.) You may also bypass sanitization for values you know are safe. To do so, bind to
127+
* an explicitly trusted value via {@link ng.$sce#trustAsHtml $sce.trustAsHtml}. See the example
128+
* under {@link ng.$sce#Example Strict Contextual Escaping (SCE)}.
126129
*
127-
* See {@link ngSanitize.$sanitize $sanitize} docs for examples.
130+
* Note: If a `$sanitize` service is unavailable and the bound value isn't explicitly trusted, you
131+
* will have an exception (instead of an exploit.)
128132
*
129133
* @element ANY
130-
* @param {expression} ngBindHtmlUnsafe {@link guide/expression Expression} to evaluate.
134+
* @param {expression} ngBindHtml {@link guide/expression Expression} to evaluate.
131135
*/
132-
var ngBindHtmlUnsafeDirective = ['$sce', function($sce) {
136+
var ngBindHtmlDirective = ['$sce', function($sce) {
133137
return function(scope, element, attr) {
134-
element.addClass('ng-binding').data('$binding', attr.ngBindHtmlUnsafe);
135-
scope.$watch($sce.parseAsHtml(attr.ngBindHtmlUnsafe), function ngBindHtmlUnsafeWatchAction(value) {
138+
element.addClass('ng-binding').data('$binding', attr.ngBindHtml);
139+
scope.$watch($sce.parseAsHtml(attr.ngBindHtml), function ngBindHtmlWatchAction(value) {
136140
element.html(value || '');
137141
});
138142
};

src/ng/sce.js

+51-60
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,17 @@ function $SceDelegateProvider() {
137137
(documentProtocol === "http:" && resourceProtocol === "https:"));
138138
}
139139

140-
this.$get = ['$log', '$document', '$$urlUtils', function(
141-
$log, $document, $$urlUtils) {
140+
this.$get = ['$log', '$document', '$injector', '$$urlUtils', function(
141+
$log, $document, $injector, $$urlUtils) {
142+
143+
var htmlSanitizer = function htmlSanitizer(html) {
144+
throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.');
145+
};
146+
147+
if ($injector.has('$sanitize')) {
148+
htmlSanitizer = $injector.get('$sanitize');
149+
}
150+
142151

143152
function matchUrl(matcher, parsedUrl) {
144153
if (matcher === 'self') {
@@ -285,6 +294,9 @@ function $SceDelegateProvider() {
285294
if (constructor && maybeTrusted instanceof constructor) {
286295
return maybeTrusted.$$unwrapTrustedValue();
287296
}
297+
// If we get here, then we may only take one of two actions.
298+
// 1. sanitize the value for the requested type, or
299+
// 2. throw an exception.
288300
if (type === SCE_CONTEXTS.RESOURCE_URL) {
289301
if (isResourceUrlAllowedByPolicy(maybeTrusted)) {
290302
return maybeTrusted;
@@ -293,6 +305,8 @@ function $SceDelegateProvider() {
293305
'Blocked loading resource from url not allowed by $sceDelegate policy. URL: {0}', maybeTrusted.toString());
294306
return;
295307
}
308+
} else if (type === SCE_CONTEXTS.HTML) {
309+
return htmlSanitizer(maybeTrusted);
296310
}
297311
throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.');
298312
}
@@ -329,8 +343,8 @@ function $SceDelegateProvider() {
329343
*
330344
* Strict Contextual Escaping (SCE) is a mode in which AngularJS requires bindings in certain
331345
* contexts to result in a value that is marked as safe to use for that context One example of such
332-
* a context is binding arbitrary html controlled by the user via `ng-bind-html-unsafe`. We refer
333-
* to these contexts as privileged or SCE contexts.
346+
* a context is binding arbitrary html controlled by the user via `ng-bind-html`. We refer to these
347+
* contexts as privileged or SCE contexts.
334348
*
335349
* As of version 1.2, Angular ships with SCE enabled by default.
336350
*
@@ -347,10 +361,10 @@ function $SceDelegateProvider() {
347361
*
348362
* <pre class="prettyprint">
349363
* <input ng-model="userHtml">
350-
* <div ng-bind-html-unsafe="{{userHtml}}">
364+
* <div ng-bind-html="{{userHtml}}">
351365
* </pre>
352366
*
353-
* Notice that `ng-bind-html-unsafe` is bound to `{{userHtml}}` controlled by the user. With SCE
367+
* Notice that `ng-bind-html` is bound to `{{userHtml}}` controlled by the user. With SCE
354368
* disabled, this application allows the user to render arbitrary HTML into the DIV.
355369
* In a more realistic example, one may be rendering user comments, blog articles, etc. via
356370
* bindings. (HTML is just one example of a context where rendering user controlled input creates
@@ -384,14 +398,14 @@ function $SceDelegateProvider() {
384398
* ng.$sce#parse $sce.parseAs} rather than `$parse` to watch attribute bindings, which performs the
385399
* {@link ng.$sce#getTrusted $sce.getTrusted} behind the scenes on non-constant literals.
386400
*
387-
* As an example, {@link ng.directive:ngBindHtmlUnsafe ngBindHtmlUnsafe} uses {@link
401+
* As an example, {@link ng.directive:ngBindHtml ngBindHtml} uses {@link
388402
* ng.$sce#parseHtml $sce.parseAsHtml(binding expression)}. Here's the actual code (slightly
389403
* simplified):
390404
*
391405
* <pre class="prettyprint">
392-
* var ngBindHtmlUnsafeDirective = ['$sce', function($sce) {
406+
* var ngBindHtmlDirective = ['$sce', function($sce) {
393407
* return function(scope, element, attr) {
394-
* scope.$watch($sce.parseAsHtml(attr.ngBindHtmlUnsafe), function(value) {
408+
* scope.$watch($sce.parseAsHtml(attr.ngBindHtml), function(value) {
395409
* element.html(value || '');
396410
* });
397411
* };
@@ -444,7 +458,7 @@ function $SceDelegateProvider() {
444458
*
445459
* | Context | Notes |
446460
* |=====================|================|
447-
* | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtmlUnsafe ngBindHtmlUnsafe} directive uses this context for bindings. |
461+
* | `$sce.HTML` | For HTML that's safe to source into the application. The {@link ng.directive:ngBindHtml ngBindHtml} directive uses this context for bindings. |
448462
* | `$sce.CSS` | For CSS that's safe to source into the application. Currently unused. Feel free to use it in your own directives. |
449463
* | `$sce.URL` | For URLs that are safe to follow as links. Currently unused (`<a href=` and `<img src=` sanitize their urls and don't consititute an SCE context. |
450464
* | `$sce.RESOURCE_URL` | For URLs that are not only safe to follow as links, but whose contens are also safe to include in your application. Examples include `ng-include`, `src` / `ngSrc` bindings for tags other than `IMG` (e.g. `IFRAME`, `OBJECT`, etc.) <br><br>Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. |
@@ -458,61 +472,37 @@ function $SceDelegateProvider() {
458472
<example module="mySceApp">
459473
<file name="index.html">
460474
<div ng-controller="myAppController as myCtrl">
461-
<button ng-click="myCtrl.fetchUserComments()" id="fetchBtn">Fetch Comments</button>
462-
<div ng-show="myCtrl.errorMsg">Error: {{myCtrl.errorMsg}}</div>
463-
<div ng-repeat="userComment in myCtrl.userComments">
464-
<hr>
465-
<b>{{userComment.name}}</b>:
466-
<span ng-bind-html-unsafe="userComment.htmlComment" class="htmlComment"></span>
475+
<i ng-bind-html="myCtrl.explicitlyTrustedHtml" id="explicitlyTrustedHtml"></i><br><br>
476+
<b>User comments</b><br>
477+
By default, HTML that isn't explicitly trusted (e.g. Alice's comment) is sanitized when $sanitize is available. If $sanitize isn't available, this results in an error instead of an exploit.
478+
<div class="well">
479+
<div ng-repeat="userComment in myCtrl.userComments">
480+
<b>{{userComment.name}}</b>:
481+
<span ng-bind-html="userComment.htmlComment" class="htmlComment"></span>
482+
<br>
483+
</div>
467484
</div>
468-
<div ng-bind-html-unsafe="myCtrl.someHtml" id="someHtml"></div>
469485
</div>
470486
</file>
471487
472488
<file name="script.js">
473-
// These types of functions would be in the data access layer of your application code.
474-
function fetchUserCommentsFromServer($http, $q, $templateCache, $sce) {
475-
var deferred = $q.defer();
476-
$http({method: "GET", url: "test_data.json", cache: $templateCache}).
477-
success(function(userComments, status) {
478-
// The comments coming from the server have been sanitized by the server and can be
479-
// trusted.
480-
angular.forEach(userComments, function(userComment) {
481-
userComment.htmlComment = $sce.trustAsHtml(userComment.htmlComment);
482-
});
483-
deferred.resolve(userComments);
484-
}).
485-
error(function (data, status) {
486-
deferred.reject("HTTP status code " + status + ": " + data);
487-
});
488-
return deferred.promise;
489-
};
490-
491-
var mySceApp = angular.module('mySceApp', []);
489+
var mySceApp = angular.module('mySceApp', ['ngSanitize']);
492490
493-
mySceApp.controller("myAppController", function myAppController($injector) {
491+
mySceApp.controller("myAppController", function myAppController($http, $templateCache, $sce) {
494492
var self = this;
495-
496-
self.someHtml = "This might have been any binding including an input element " +
497-
"controlled by the user.";
498-
499-
self.fetchUserComments = function() {
500-
$injector.invoke(fetchUserCommentsFromServer).then(
501-
function onSuccess(userComments) {
502-
self.errorMsg = null;
503-
self.userComments = userComments;
504-
},
505-
function onFailure(errorMsg) {
506-
self.errorMsg = errorMsg;
507-
});
508-
}
493+
$http.get("test_data.json", {cache: $templateCache}).success(function(userComments) {
494+
self.userComments = userComments;
495+
});
496+
self.explicitlyTrustedHtml = $sce.trustAsHtml(
497+
'<span onmouseover="this.textContent=&quot;Explicitly trusted HTML bypasses ' +
498+
'sanitization.&quot;">Hover over this text.</span>');
509499
});
510500
</file>
511501
512502
<file name="test_data.json">
513503
[
514504
{ "name": "Alice",
515-
"htmlComment": "Is <i>anyone</i> reading this?"
505+
"htmlComment": "<span onmouseover='this.textContent=\"PWN3D!\"'>Is <i>anyone</i> reading this?</span>"
516506
},
517507
{ "name": "Bob",
518508
"htmlComment": "<i>Yes!</i> Am I the only other one?"
@@ -521,14 +511,15 @@ function $SceDelegateProvider() {
521511
</file>
522512
523513
<file name="scenario.js">
524-
describe('SCE doc demo', function() {
525-
it('should bind trusted values', function() {
526-
element('#fetchBtn').click();
527-
expect(element('.htmlComment').html()).toBe('Is <i>anyone</i> reading this?');
528-
});
529-
it('should NOT bind arbitrary values', function() {
530-
expect(element('#someHtml').html()).toBe('');
531-
});
514+
describe('SCE doc demo', function() {
515+
it('should sanitize untrusted values', function() {
516+
expect(element('.htmlComment').html()).toBe('<span>Is <i>anyone</i> reading this?</span>');
517+
});
518+
it('should NOT sanitize explicitly trusted values', function() {
519+
expect(element('#explicitlyTrustedHtml').html()).toBe(
520+
'<span onmouseover="this.textContent=&quot;Explicitly trusted HTML bypasses ' +
521+
'sanitization.&quot;">Hover over this text.</span>');
522+
});
532523
});
533524
</file>
534525
</example>

src/ngSanitize/directive/ngBindHtml.js

-25
This file was deleted.

src/ngSanitize/sanitize.js

+32-33
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,7 @@ var ngSanitizeMinErr = angular.$$minErr('ngSanitize');
6868
'<p style="color:blue">an html\n' +
6969
'<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
7070
'snippet</p>';
71-
// ng-bind-html-unsafe requires a $sce trusted value of type $sce.HTML.
72-
$scope.getSceSnippet = function() {
71+
$scope.deliberatelyTrustDangerousSnippet = function() {
7372
return $sce.trustAsHtml($scope.snippet);
7473
};
7574
}
@@ -78,57 +77,57 @@ var ngSanitizeMinErr = angular.$$minErr('ngSanitize');
7877
Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
7978
<table>
8079
<tr>
81-
<td>Filter</td>
80+
<td>Directive</td>
81+
<td>How</td>
8282
<td>Source</td>
8383
<td>Rendered</td>
8484
</tr>
85-
<tr id="html-filter">
86-
<td>html filter</td>
87-
<td>
88-
<pre>&lt;div ng-bind-html="snippet"&gt;<br/>&lt;/div&gt;</pre>
89-
</td>
90-
<td>
91-
<div ng-bind-html="snippet"></div>
92-
</td>
85+
<tr id="bind-html-with-sanitize">
86+
<td>ng-bind-html</td>
87+
<td>Automatically uses $sanitize</td>
88+
<td><pre>&lt;div ng-bind-html="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
89+
<td><div ng-bind-html="snippet"></div></td>
9390
</tr>
94-
<tr id="escaped-html">
95-
<td>no filter</td>
91+
<tr id="bind-html-with-trust">
92+
<td>ng-bind-html</td>
93+
<td>Bypass $sanitize by explicitly trusting the dangerous value</td>
94+
<td><pre>&lt;div ng-bind-html="deliberatelyTrustDangerousSnippet()"&gt;<br/>&lt;/div&gt;</pre></td>
95+
<td><div ng-bind-html="deliberatelyTrustDangerousSnippet()"></div></td>
96+
</tr>
97+
<tr id="bind-default">
98+
<td>ng-bind</td>
99+
<td>Automatically escapes</td>
96100
<td><pre>&lt;div ng-bind="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
97101
<td><div ng-bind="snippet"></div></td>
98102
</tr>
99-
<tr id="html-unsafe-filter">
100-
<td>unsafe html filter</td>
101-
<td><pre>&lt;div ng-bind-html-unsafe="getSceSnippet()"&gt;<br/>&lt;/div&gt;</pre></td>
102-
<td><div ng-bind-html-unsafe="getSceSnippet()"></div></td>
103-
</tr>
104103
</table>
105104
</div>
106105
</doc:source>
107106
<doc:scenario>
108-
it('should sanitize the html snippet ', function() {
109-
expect(using('#html-filter').element('div').html()).
107+
it('should sanitize the html snippet by default', function() {
108+
expect(using('#bind-html-with-sanitize').element('div').html()).
110109
toBe('<p>an html\n<em>click here</em>\nsnippet</p>');
111110
});
112111
112+
it('should inline raw snippet if bound to a trusted value', function() {
113+
expect(using('#bind-html-with-trust').element("div").html()).
114+
toBe("<p style=\"color:blue\">an html\n" +
115+
"<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
116+
"snippet</p>");
117+
});
118+
113119
it('should escape snippet without any filter', function() {
114-
expect(using('#escaped-html').element('div').html()).
120+
expect(using('#bind-default').element('div').html()).
115121
toBe("&lt;p style=\"color:blue\"&gt;an html\n" +
116122
"&lt;em onmouseover=\"this.textContent='PWN3D!'\"&gt;click here&lt;/em&gt;\n" +
117123
"snippet&lt;/p&gt;");
118124
});
119125
120-
it('should inline raw snippet if filtered as unsafe', function() {
121-
expect(using('#html-unsafe-filter').element("div").html()).
122-
toBe("<p style=\"color:blue\">an html\n" +
123-
"<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
124-
"snippet</p>");
125-
});
126-
127-
it('should update', function($sce) {
128-
input('snippet').enter('new <b>text</b>');
129-
expect(using('#html-filter').binding('snippet')).toBe('new <b>text</b>');
130-
expect(using('#escaped-html').element('div').html()).toBe("new &lt;b&gt;text&lt;/b&gt;");
131-
expect(using('#html-unsafe-filter').element('div').html()).toBe('new <b>text</b>');
126+
it('should update', function() {
127+
input('snippet').enter('new <b onclick="alert(1)">text</b>');
128+
expect(using('#bind-html-with-sanitize').element('div').html()).toBe('new <b>text</b>');
129+
expect(using('#bind-html-with-trust').element('div').html()).toBe('new <b onclick="alert(1)">text</b>');
130+
expect(using('#bind-default').element('div').html()).toBe("new &lt;b onclick=\"alert(1)\"&gt;text&lt;/b&gt;");
132131
});
133132
</doc:scenario>
134133
</doc:example>

0 commit comments

Comments
 (0)