From 189be3f29c12f4d9a67c3c6202a5f1b1a1588cbb Mon Sep 17 00:00:00 2001 From: Shahar Talmi Date: Sat, 17 May 2014 16:05:37 +0300 Subject: [PATCH] feat($interpolate): escape interpolated expressions the default escaped interpolation signs are `{{{{` and `}}}}`. those symbols will be ignored when parsing the interpolated string and will only be replaced by `{{` and `}}` in the result string. this allows servers that put unsafe strings inside html templates to replace `{{` with `{{{{` and optionally `}}` with `}}}}` in order to prevent XSS attacks. Closes #5601 --- src/ng/interpolate.js | 38 +++++++++++++++++++++++++++++------- test/ng/interpolateSpec.js | 40 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/src/ng/interpolate.js b/src/ng/interpolate.js index e529bc1bf420..9d9bdb0578a0 100644 --- a/src/ng/interpolate.js +++ b/src/ng/interpolate.js @@ -41,6 +41,8 @@ var $interpolateMinErr = minErr('$interpolate'); function $InterpolateProvider() { var startSymbol = '{{'; var endSymbol = '}}'; + var escapedStartSymbol = '{{{{'; + var escapedEndSymbol = '}}}}'; /** * @ngdoc method @@ -49,11 +51,15 @@ function $InterpolateProvider() { * Symbol to denote start of expression in the interpolated string. Defaults to `{{`. * * @param {string=} value new value to set the starting symbol to. + * @param {string=} escaped new value to set the escaped starting symbol to. * @returns {string|self} Returns the symbol when used as getter and self if used as setter. */ - this.startSymbol = function(value){ + this.startSymbol = function(value, escaped) { if (value) { startSymbol = value; + if (escaped) { + escapedStartSymbol = escaped; + } return this; } else { return startSymbol; @@ -67,11 +73,15 @@ function $InterpolateProvider() { * Symbol to denote the end of expression in the interpolated string. Defaults to `}}`. * * @param {string=} value new value to set the ending symbol to. + * @param {string=} escaped new value to set the escaped ending symbol to. * @returns {string|self} Returns the symbol when used as getter and self if used as setter. */ - this.endSymbol = function(value){ + this.endSymbol = function(value, escaped) { if (value) { endSymbol = value; + if (escaped) { + escapedEndSymbol = escaped; + } return this; } else { return endSymbol; @@ -81,7 +91,9 @@ function $InterpolateProvider() { this.$get = ['$parse', '$exceptionHandler', '$sce', function($parse, $exceptionHandler, $sce) { var startSymbolLength = startSymbol.length, - endSymbolLength = endSymbol.length; + endSymbolLength = endSymbol.length, + escapedStartLength = escapedStartSymbol.length, + escapedStartOffset = escapedStartSymbol.indexOf(startSymbol); /** * @ngdoc service @@ -157,10 +169,17 @@ function $InterpolateProvider() { lastValuesCache = { values: {}, results: {}}; while(index < textLength) { - if ( ((startIndex = text.indexOf(startSymbol, index)) != -1) && - ((endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1) ) { + var i = index; + do { + startIndex = text.indexOf(startSymbol, i); + i = startIndex - escapedStartOffset + escapedStartLength; + } while (escapedStartOffset !== -1 && startIndex !== -1 && + text.slice(startIndex - escapedStartOffset, i) === escapedStartSymbol); + + if (startIndex !== -1 && + (endIndex = text.indexOf(endSymbol, startIndex + startSymbolLength)) != -1) { if (index !== startIndex) hasText = true; - separators.push(text.substring(index, startIndex)); + separators.push(unescape(text.substring(index, startIndex))); exp = text.substring(startIndex + startSymbolLength, endIndex); expressions.push(exp); parseFns.push($parse(exp)); @@ -170,7 +189,7 @@ function $InterpolateProvider() { // we did not find an interpolation, so we have to add the remainder to the separators array if (index !== textLength) { hasText = true; - separators.push(text.substring(index)); + separators.push(unescape(text.substring(index))); } break; } @@ -316,6 +335,11 @@ function $InterpolateProvider() { return endSymbol; }; + function unescape(text) { + return text.split(escapedStartSymbol).join(startSymbol) + .split(escapedEndSymbol).join(endSymbol); + } + return $interpolate; }]; } diff --git a/test/ng/interpolateSpec.js b/test/ng/interpolateSpec.js index 6dd49d6bdaae..b1c3e807a613 100644 --- a/test/ng/interpolateSpec.js +++ b/test/ng/interpolateSpec.js @@ -60,6 +60,46 @@ describe('$interpolate', function() { expect($interpolate("Hello, world!{{bloop}}")()).toBe("Hello, world!"); })); + 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'); + })); + + it('should not really care about end symbols when escaping', inject(function($interpolate) { + expect($interpolate('{{{{foo{{foo}}')(obj)).toBe('{{fooHello'); + })); + + it('should support customizing escape signs', function() { + module(function($interpolateProvider) { + $interpolateProvider.startSymbol('{{', '[['); + $interpolateProvider.endSymbol('}}', ']]'); + }); + inject(function($interpolate) { + expect($interpolate('{{foo}} [[bar]]')(obj)).toBe('Hello {{bar}}'); + }); + }); + + it('should support customizing escape signs which contain interpolation signs', function() { + module(function($interpolateProvider) { + $interpolateProvider.startSymbol('{{', '-{{-'); + $interpolateProvider.endSymbol('}}', '-}}-'); + }); + inject(function($interpolate) { + expect($interpolate('{{foo}} -{{-bar-}}-')(obj)).toBe('Hello {{bar}}'); + }); + }); + }); describe('interpolating in a trusted context', function() { var sce;