Description
AngularJS Strict Contextual Escaping (SCE) applies context-specific trust checks to HTML and sensitive URL bindings. For HTML, it sanitizes untrusted values when $sanitize is available and rejects them otherwise. It does not automatically sanitize all CSS or JavaScript. Disabling SCE globally with $sceProvider.enabled(false) may lead to XSS when untrusted values reach bindings such as ng-bind-html. Disabling it in a shared module also affects the whole application.
Official AngularJS support ended in January 2022. This guidance is for maintaining existing AngularJS applications.
Potential impact
- XSS may execute malicious JavaScript in the document's context, changing the interface or enabling further attacks.
- Cookies or tokens accessible to the page may be stolen and used to take over a session.
- Data displayed on or accessible to the page, such as personal information or CSRF tokens, may be sent elsewhere.
- DOM changes may display fake input or payment forms for phishing.
- Disabled SCE combined with insufficient input validation, CSP, or other defenses may increase the damage.
Remediation
- Do not use
$sceProvider.enabled(false). Keep SCE enabled by default. - Handle dynamic content as follows:
- Use
ng-bindfor text to avoid interpreting HTML. - If HTML is necessary, sanitize it with
$sanitizefromngSanitize. Use$sce.trustAsHtml(cleanHtml)only when required to mark the sanitized value as trusted in that context.
- Use
- Apply allow-list sanitization to user-generated content, such as comments or profiles, on the server and client before storing or displaying it.
- Restrict the sources of
ng-bind-htmlvalues and bind only trusted data. - Check through code review and static analysis (SAST) that third-party scripts or modules do not disable SCE.
- Apply Content-Security-Policy (CSP) to limit inline scripts and arbitrary script loading.
Examples
Before
Each block combines excerpts from separate app.js and index.html files.
javascript
<!-- app.js -->
(function(){
angular.module('shopApp', [])
.config(['$sceProvider', function($sceProvider){
// BAD: disable SCE globally
$sceProvider.enabled(false);
}])
.controller('CommentCtrl', ['$location', function($location){
var vm = this;
// An attacker can inject through the URL ?c=<img src=x onerror=alert(1)>
vm.comment = $location.search().c || '';
}]);
})();
<!-- index.html -->
<div ng-app="shopApp" ng-controller="CommentCtrl as vm">
<!-- BAD: render HTML directly with SCE disabled -->
<div ng-bind-html="vm.comment"></div>
</div>
After
javascript
<!-- app.js -->
(function(){
angular.module('shopApp', ['ngSanitize'])
// SCE is not disabled; keep the enabled default
.controller('CommentCtrl', ['$location', '$sanitize', '$sce', function($location, $sanitize, $sce){
var vm = this;
var raw = $location.search().c || '';
// 1) Safest option: display as text only
vm.commentText = raw;
// 2) Only when dynamic HTML is necessary: sanitize before marking trusted
var clean = $sanitize(raw);
vm.commentHtml = $sce.trustAsHtml(clean);
}]);
})();
<!-- index.html -->
<div ng-app="shopApp" ng-controller="CommentCtrl as vm">
<!-- Render text without interpreting HTML -->
<div ng-bind="vm.commentText"></div>
<!-- Use only when needed: bind the result of $sanitize + trustAsHtml -->
<div ng-bind-html="vm.commentHtml"></div>
</div>
Explanation:
- Before: With SCE checks disabled by
$sceProvider.enabled(false), passing URL input tong-bind-htmlmay render dangerous HTML. - After: SCE remains enabled, and
ng-binddisplays text by default. When HTML is needed,$sanitizeprocesses it first.$sce.trustAsHtmlmarks a value as trusted; it is not itself a sanitizer.