From 7cbb1044fcb3576cdad791bd22ebea3dfd533ff8 Mon Sep 17 00:00:00 2001 From: Jason Bedard Date: Thu, 27 Sep 2018 01:00:54 -0700 Subject: [PATCH] fix(input): prevent browsers from autofilling hidden inputs Autofilling with previous values (which will then be `$interpolate`ed) could lead to XSS or errors --- src/AngularPublic.js | 5 +- src/ng/directive/input.js | 42 +++++++++++++++++ test/e2e/fixtures/back2dom/index.html | 13 +++++ test/e2e/fixtures/back2dom/script.js | 11 +++++ test/e2e/tests/input-hidden.spec.js | 68 +++++++++++++++++++++++++++ 5 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 test/e2e/fixtures/back2dom/index.html create mode 100644 test/e2e/fixtures/back2dom/script.js diff --git a/src/AngularPublic.js b/src/AngularPublic.js index 725e2877078f..760fa835bd48 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -7,7 +7,7 @@ htmlAnchorDirective, inputDirective, - inputDirective, + hiddenInputBrowserCacheDirective, formDirective, scriptDirective, selectDirective, @@ -221,7 +221,8 @@ function publishExternalAPI(angular) { ngModelOptions: ngModelOptionsDirective }). directive({ - ngInclude: ngIncludeFillContentDirective + ngInclude: ngIncludeFillContentDirective, + input: hiddenInputBrowserCacheDirective }). directive(ngAttributeAliasDirectives). directive(ngEventDirectives); diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index bf6576f81a09..2f75defe1944 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -2193,6 +2193,48 @@ var inputDirective = ['$browser', '$sniffer', '$filter', '$parse', }]; +var hiddenInputBrowserCacheDirective = function() { + var valueProperty = { + configurable: true, + enumerable: false, + get: function() { + return this.getAttribute('value') || ''; + }, + set: function(val) { + this.setAttribute('value', val); + } + }; + + return { + restrict: 'E', + priority: 200, + compile: function(_, attr) { + if (lowercase(attr.type) !== 'hidden') { + return; + } + + return { + pre: function(scope, element, attr, ctrls) { + var node = element[0]; + + // Support: Edge + // Moving the DOM around prevents autofillling + if (node.parentNode) { + node.parentNode.insertBefore(node, node.nextSibling); + } + + // Support: FF, IE + // Avoiding direct assignment to .value prevents autofillling + if (Object.defineProperty) { + Object.defineProperty(node, 'value', valueProperty); + } + } + }; + } + }; +}; + + var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/; /** diff --git a/test/e2e/fixtures/back2dom/index.html b/test/e2e/fixtures/back2dom/index.html new file mode 100644 index 000000000000..6134b796a1fa --- /dev/null +++ b/test/e2e/fixtures/back2dom/index.html @@ -0,0 +1,13 @@ + + + +
+ + + + +
+ + + + \ No newline at end of file diff --git a/test/e2e/fixtures/back2dom/script.js b/test/e2e/fixtures/back2dom/script.js new file mode 100644 index 000000000000..04911865c39d --- /dev/null +++ b/test/e2e/fixtures/back2dom/script.js @@ -0,0 +1,11 @@ +'use strict'; + +angular + .module('test', []) + .run(function($rootScope) { + $rootScope.internalFnCalled = false; + + $rootScope.internalFn = function() { + $rootScope.internalFnCalled = true; + }; + }); diff --git a/test/e2e/tests/input-hidden.spec.js b/test/e2e/tests/input-hidden.spec.js index ef2669f0f64a..e1e76e0390a4 100644 --- a/test/e2e/tests/input-hidden.spec.js +++ b/test/e2e/tests/input-hidden.spec.js @@ -14,4 +14,72 @@ describe('hidden thingy', function() { var expectedValue = browser.params.browser === 'safari' ? '{{ 7 * 6 }}' : ''; expect(element(by.css('input')).getAttribute('value')).toEqual(expectedValue); }); + + it('should prevent browser autofill on browser.refresh', function() { + + loadFixture('back2dom'); + expect(element(by.css('#input1')).getAttribute('value')).toEqual(''); + expect(element(by.css('#input2')).getAttribute('value')).toEqual(''); + + element(by.css('textarea')).sendKeys('{{ internalFn() }}'); + + expect(element(by.css('#input1')).getAttribute('value')).toEqual('{{ internalFn() }}'); + expect(element(by.css('#input2')).getAttribute('value')).toEqual('{{ internalFn() }}'); + expect(element(by.css('body')).getAttribute('class')).toBe(''); + + browser.refresh(); + expect(element(by.css('body')).getAttribute('class')).toBe(''); + }); + + it('should prevent browser autofill on location.reload', function() { + + loadFixture('back2dom'); + expect(element(by.css('#input1')).getAttribute('value')).toEqual(''); + expect(element(by.css('#input2')).getAttribute('value')).toEqual(''); + + element(by.css('textarea')).sendKeys('{{ internalFn() }}'); + + expect(element(by.css('#input1')).getAttribute('value')).toEqual('{{ internalFn() }}'); + expect(element(by.css('#input2')).getAttribute('value')).toEqual('{{ internalFn() }}'); + expect(element(by.css('body')).getAttribute('class')).toBe(''); + + browser.driver.executeScript('location.reload()'); + expect(element(by.css('body')).getAttribute('class')).toBe(''); + }); + + it('should prevent browser autofill on history.back', function() { + + loadFixture('back2dom'); + expect(element(by.css('#input1')).getAttribute('value')).toEqual(''); + expect(element(by.css('#input2')).getAttribute('value')).toEqual(''); + + element(by.css('textarea')).sendKeys('{{ internalFn() }}'); + + expect(element(by.css('#input1')).getAttribute('value')).toEqual('{{ internalFn() }}'); + expect(element(by.css('#input2')).getAttribute('value')).toEqual('{{ internalFn() }}'); + expect(element(by.css('body')).getAttribute('class')).toBe(''); + + loadFixture('sample'); + + browser.driver.executeScript('history.back()'); + expect(element(by.css('body')).getAttribute('class')).toBe(''); + }); + + it('should prevent browser autofill on history.forward', function() { + + loadFixture('sample'); + loadFixture('back2dom'); + expect(element(by.css('#input1')).getAttribute('value')).toEqual(''); + expect(element(by.css('#input2')).getAttribute('value')).toEqual(''); + + element(by.css('textarea')).sendKeys('{{ internalFn() }}'); + + expect(element(by.css('#input1')).getAttribute('value')).toEqual('{{ internalFn() }}'); + expect(element(by.css('#input2')).getAttribute('value')).toEqual('{{ internalFn() }}'); + expect(element(by.css('body')).getAttribute('class')).toBe(''); + + browser.driver.executeScript('history.back()'); + browser.driver.executeScript('history.forward()'); + expect(element(by.css('body')).getAttribute('class')).toBe(''); + }); });