Skip to content

Commit

Permalink
feat($interpolate): escaped interpolation expressions
Browse files Browse the repository at this point in the history
This CL enables interpolation expressions to be escaped, by prefixing each character of their
start/end markers with a REVERSE SOLIDUS U+005C, and to render the escaped expression as a
regular interpolation expression.

Example:

`<span ng-init="foo='Hello'">{{foo}}, \\{\\{World!\\}\\}</span>` would be rendered as:
`<span ng-init="foo='Hello'">Hello, {{World!}}</span>`

This will also work with custom interpolation markers, for example:

     module.
       config(function($interpolateProvider) {
         $interpolateProvider.startSymbol('\\\\');
         $interpolateProvider.endSymbol('//');
       }).
       run(function($interpolate) {
         // Will alert with "hello\\bar//":
         alert($interpolate('\\\\foo//\\\\\\\\bar\\/\\/')({foo: "hello", bar: "world"}));
       });

This change effectively only changes the rendering of these escaped markers, because they are
not context-aware, and are incapable of preventing nested expressions within those escaped
markers from being evaluated.

Therefore, backends are encouraged to ensure that when escaping expressions for security
reasons, every single instance of a start or end marker have each of its characters prefixed
with a backslash (REVERSE SOLIDUS, U+005C)

Closes angular#5601
Closes angular#7517
  • Loading branch information
caitp authored and RichardLitt committed May 24, 2014
1 parent 770fd4f commit 5ea5dd5
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 1 deletion.
50 changes: 49 additions & 1 deletion src/ng/interpolate.js
Expand Up @@ -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
Expand Down Expand Up @@ -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 (&lt;, &gt;) with &amp;lt; and &amp;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.
*
* <example>
* <file name="index.html">
* <div ng-init="username='A user'">
* <p ng-init="apptitle='Escaping demo'">{{apptitle}}: \{\{ username = "some jerk"; \}\}
* </p>
* <p><strong>{{username}}</strong> 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.</p>
* <p>Instead, the result of the attempted script injection is visible, and can be removed
* from the database by an administrator.</p>
* </div>
* </file>
* </example>
*
* @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
Expand Down Expand Up @@ -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('');
}
Expand Down
60 changes: 60 additions & 0 deletions test/ng/interpolateSpec.js
Expand Up @@ -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() {
Expand Down

0 comments on commit 5ea5dd5

Please sign in to comment.