diff --git a/src/ng/interpolate.js b/src/ng/interpolate.js index f3209a9800a0..a96ca10d29da 100644 --- a/src/ng/interpolate.js +++ b/src/ng/interpolate.js @@ -81,7 +81,13 @@ function $InterpolateProvider() { this.$get = ['$parse', '$exceptionHandler', '$sce', function($parse, $exceptionHandler, $sce) { var startSymbolLength = startSymbol.length, - endSymbolLength = endSymbol.length; + endSymbolLength = endSymbol.length, + escapedStartRegexp = new RegExp(startSymbol.replace(/./g, escape), 'g'), + escapedEndRegexp = new RegExp(endSymbol.replace(/./g, escape), 'g'); + + function escape(ch) { + return '\\\\\\' + ch; + } /** * @ngdoc service @@ -126,6 +132,42 @@ function $InterpolateProvider() { * * `allOrNothing` is useful for interpolating URLs. `ngSrc` and `ngSrcset` use this behavior. * + * ####Escaped Interpolation + * $interpolate provides a mechanism for escaping interpolation markers. Start and end markers + * can be escaped by preceding each of their characters with a REVERSE SOLIDUS U+005C (backslash). + * It will be rendered as a regular start/end marker, and will not be interpreted as an expression + * or binding. + * + * This enables web-servers to prevent script injection attacks and defacing attacks, to some + * degree, while also enabling code examples to work without relying on the + * {@link ng.directive:ngNonBindable ngNonBindable} directive. + * + * **For security purposes, it is strongly encouraged that web servers escape user-supplied data, + * replacing angle brackets (<, >) with &lt; and &gt; respectively, and replacing all + * interpolation start/end markers with their escaped counterparts.** + * + * Escaped interpolation markers are only replaced with the actual interpolation markers in rendered + * output when the $interpolate service processes the text. So, for HTML elements interpolated + * by {@link ng.$compile $compile}, or otherwise interpolated with the `mustHaveExpression` parameter + * set to `true`, the interpolated text must contain an unescaped interpolation expression. As such, + * this is typically useful only when user-data is used in rendering a template from the server, or + * when otherwise untrusted data is used by a directive. + * + * + * + *
+ *

{{apptitle}}: \{\{ username = "some jerk"; \}\} + *

+ *

{{username}} attempts to inject code which will deface the + * application, but fails to accomplish their task, because the server has correctly + * escaped the interpolation start/end markers with REVERSE SOLIDUS U+005C (backslash) + * characters.

+ *

Instead, the result of the attempted script injection is visible, and can be removed + * from the database by an administrator.

+ *
+ *
+ *
+ * * @param {string} text The text with markup to interpolate. * @param {boolean=} mustHaveExpression if set to true then the interpolation string must have * embedded expression in order to return an interpolation function. Strings with no @@ -176,6 +218,12 @@ function $InterpolateProvider() { } } + forEach(separators, function(key, i) { + separators[i] = separators[i]. + replace(escapedStartRegexp, startSymbol). + replace(escapedEndRegexp, endSymbol); + }); + if (separators.length === expressions.length) { separators.push(''); } diff --git a/test/ng/interpolateSpec.js b/test/ng/interpolateSpec.js index 6dd49d6bdaae..0bc767339062 100644 --- a/test/ng/interpolateSpec.js +++ b/test/ng/interpolateSpec.js @@ -61,6 +61,66 @@ describe('$interpolate', function() { })); + describe('interpolation escaping', function() { + var obj; + beforeEach(function() { + obj = {foo: 'Hello', bar: 'World'}; + }); + + + it('should support escaping interpolation signs', inject(function($interpolate) { + expect($interpolate('{{foo}} \\{\\{bar\\}\\}')(obj)).toBe('Hello {{bar}}'); + expect($interpolate('\\{\\{foo\\}\\} {{bar}}')(obj)).toBe('{{foo}} World'); + })); + + + it('should unescape multiple expressions', inject(function($interpolate) { + expect($interpolate('\\{\\{foo\\}\\}\\{\\{bar\\}\\} {{foo}}')(obj)).toBe('{{foo}}{{bar}} Hello'); + expect($interpolate('{{foo}}\\{\\{foo\\}\\}\\{\\{bar\\}\\}')(obj)).toBe('Hello{{foo}}{{bar}}'); + expect($interpolate('\\{\\{foo\\}\\}{{foo}}\\{\\{bar\\}\\}')(obj)).toBe('{{foo}}Hello{{bar}}'); + expect($interpolate('{{foo}}\\{\\{foo\\}\\}{{bar}}\\{\\{bar\\}\\}{{foo}}')(obj)).toBe('Hello{{foo}}World{{bar}}Hello'); + })); + + + it('should support escaping custom interpolation start/end symbols', function() { + module(function($interpolateProvider) { + $interpolateProvider.startSymbol('[['); + $interpolateProvider.endSymbol(']]'); + }); + inject(function($interpolate) { + expect($interpolate('[[foo]] \\[\\[bar\\]\\]')(obj)).toBe('Hello [[bar]]'); + }); + }); + + + it('should unescape incomplete escaped expressions', inject(function($interpolate) { + expect($interpolate('\\{\\{foo{{foo}}')(obj)).toBe('{{fooHello'); + expect($interpolate('\\}\\}foo{{foo}}')(obj)).toBe('}}fooHello'); + expect($interpolate('foo{{foo}}\\{\\{')(obj)).toBe('fooHello{{'); + expect($interpolate('foo{{foo}}\\}\\}')(obj)).toBe('fooHello}}'); + })); + + + it('should not unescape markers within expressions', inject(function($interpolate) { + expect($interpolate('{{"\\\\{\\\\{Hello, world!\\\\}\\\\}"}}')(obj)).toBe('\\{\\{Hello, world!\\}\\}'); + expect($interpolate('{{"\\{\\{Hello, world!\\}\\}"}}')(obj)).toBe('{{Hello, world!}}'); + expect(function() { + $interpolate('{{\\{\\{foo\\}\\}}}')(obj); + }).toThrowMinErr('$parse', 'lexerr', + 'Lexer Error: Unexpected next character at columns 0-0 [\\] in expression [\\{\\{foo\\}\\]'); + })); + + + // This test demonstrates that the web-server is responsible for escaping every single instance + // of interpolation start/end markers in an expression which they do not wish to evaluate, + // because AngularJS will not protect them from being evaluated (due to the added complexity + // and maintenance burden of context-sensitive escaping) + it('should evaluate expressions between escaped start/end symbols', inject(function($interpolate) { + expect($interpolate('\\{\\{Hello, {{bar}}!\\}\\}')(obj)).toBe('{{Hello, World!}}'); + })); + }); + + describe('interpolating in a trusted context', function() { var sce; beforeEach(function() {