Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

Commit 6e3bbfc

Browse files
committed
fix(input): prevent browsers from autofilling hidden inputs
Autofilling with previous values (which will then be `$interpolate`ed) could lead to XSS or errors
1 parent 09751be commit 6e3bbfc

File tree

5 files changed

+137
-2
lines changed

5 files changed

+137
-2
lines changed

Diff for: src/AngularPublic.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
88
htmlAnchorDirective,
99
inputDirective,
10-
inputDirective,
10+
hiddenInputBrowserCacheDirective,
1111
formDirective,
1212
scriptDirective,
1313
selectDirective,
@@ -221,7 +221,8 @@ function publishExternalAPI(angular) {
221221
ngModelOptions: ngModelOptionsDirective
222222
}).
223223
directive({
224-
ngInclude: ngIncludeFillContentDirective
224+
ngInclude: ngIncludeFillContentDirective,
225+
input: hiddenInputBrowserCacheDirective
225226
}).
226227
directive(ngAttributeAliasDirectives).
227228
directive(ngEventDirectives);

Diff for: src/ng/directive/input.js

+42
Original file line numberDiff line numberDiff line change
@@ -2193,6 +2193,48 @@ var inputDirective = ['$browser', '$sniffer', '$filter', '$parse',
21932193
}];
21942194

21952195

2196+
var hiddenInputBrowserCacheDirective = function() {
2197+
var valueProperty = {
2198+
configurable: true,
2199+
enumerable: false,
2200+
get: function() {
2201+
return this.getAttribute('value') || '';
2202+
},
2203+
set: function(val) {
2204+
this.setAttribute('value', val);
2205+
}
2206+
};
2207+
2208+
return {
2209+
restrict: 'E',
2210+
priority: 200,
2211+
compile: function(_, attr) {
2212+
if (lowercase(attr.type) !== 'hidden') {
2213+
return;
2214+
}
2215+
2216+
return {
2217+
pre: function(scope, element, attr, ctrls) {
2218+
var node = element[0];
2219+
2220+
// Support: Edge
2221+
// Moving the DOM around prevents autofillling
2222+
if (node.parentNode) {
2223+
node.parentNode.insertBefore(node, node.nextSibling);
2224+
}
2225+
2226+
// Support: FF, IE
2227+
// Avoiding direct assignment to .value prevents autofillling
2228+
if (Object.defineProperty) {
2229+
Object.defineProperty(node, 'value', valueProperty);
2230+
}
2231+
}
2232+
};
2233+
}
2234+
};
2235+
};
2236+
2237+
21962238

21972239
var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/;
21982240
/**

Diff for: test/e2e/fixtures/back2dom/index.html

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!DOCTYPE html>
2+
<html ng-app="test">
3+
<body ng-class="{hacked: internalFnCalled}">
4+
<form>
5+
<input id="input1" type="hidden" value="{{value}}" />
6+
<input id="input2" type="hidden" ng-value="value" />
7+
8+
<textarea ng-model="value"></textarea>
9+
</form>
10+
<script src="angular.js"></script>
11+
<script src="script.js"></script>
12+
</body>
13+
</html>

Diff for: test/e2e/fixtures/back2dom/script.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
'use strict';
2+
3+
angular
4+
.module('test', [])
5+
.run(function($rootScope) {
6+
$rootScope.internalFnCalled = false;
7+
8+
$rootScope.internalFn = function() {
9+
$rootScope.internalFnCalled = true;
10+
};
11+
});

Diff for: test/e2e/tests/input-hidden.spec.js

+68
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,72 @@ describe('hidden thingy', function() {
1414
var expectedValue = browser.params.browser === 'safari' ? '{{ 7 * 6 }}' : '';
1515
expect(element(by.css('input')).getAttribute('value')).toEqual(expectedValue);
1616
});
17+
18+
it('should prevent browser autofill on browser.refresh', function() {
19+
20+
loadFixture('back2dom');
21+
expect(element(by.css('#input1')).getAttribute('value')).toEqual('');
22+
expect(element(by.css('#input2')).getAttribute('value')).toEqual('');
23+
24+
element(by.css('textarea')).sendKeys('{{ internalFn() }}');
25+
26+
expect(element(by.css('#input1')).getAttribute('value')).toEqual('{{ internalFn() }}');
27+
expect(element(by.css('#input2')).getAttribute('value')).toEqual('{{ internalFn() }}');
28+
expect(element(by.css('body')).getAttribute('class')).toBe('');
29+
30+
browser.refresh();
31+
expect(element(by.css('body')).getAttribute('class')).toBe('');
32+
});
33+
34+
it('should prevent browser autofill on location.reload', function() {
35+
36+
loadFixture('back2dom');
37+
expect(element(by.css('#input1')).getAttribute('value')).toEqual('');
38+
expect(element(by.css('#input2')).getAttribute('value')).toEqual('');
39+
40+
element(by.css('textarea')).sendKeys('{{ internalFn() }}');
41+
42+
expect(element(by.css('#input1')).getAttribute('value')).toEqual('{{ internalFn() }}');
43+
expect(element(by.css('#input2')).getAttribute('value')).toEqual('{{ internalFn() }}');
44+
expect(element(by.css('body')).getAttribute('class')).toBe('');
45+
46+
browser.driver.executeScript('location.reload()');
47+
expect(element(by.css('body')).getAttribute('class')).toBe('');
48+
});
49+
50+
it('should prevent browser autofill on history.back', function() {
51+
52+
loadFixture('back2dom');
53+
expect(element(by.css('#input1')).getAttribute('value')).toEqual('');
54+
expect(element(by.css('#input2')).getAttribute('value')).toEqual('');
55+
56+
element(by.css('textarea')).sendKeys('{{ internalFn() }}');
57+
58+
expect(element(by.css('#input1')).getAttribute('value')).toEqual('{{ internalFn() }}');
59+
expect(element(by.css('#input2')).getAttribute('value')).toEqual('{{ internalFn() }}');
60+
expect(element(by.css('body')).getAttribute('class')).toBe('');
61+
62+
loadFixture('sample');
63+
64+
browser.driver.executeScript('history.back()');
65+
expect(element(by.css('body')).getAttribute('class')).toBe('');
66+
});
67+
68+
it('should prevent browser autofill on history.forward', function() {
69+
70+
loadFixture('sample');
71+
loadFixture('back2dom');
72+
expect(element(by.css('#input1')).getAttribute('value')).toEqual('');
73+
expect(element(by.css('#input2')).getAttribute('value')).toEqual('');
74+
75+
element(by.css('textarea')).sendKeys('{{ internalFn() }}');
76+
77+
expect(element(by.css('#input1')).getAttribute('value')).toEqual('{{ internalFn() }}');
78+
expect(element(by.css('#input2')).getAttribute('value')).toEqual('{{ internalFn() }}');
79+
expect(element(by.css('body')).getAttribute('class')).toBe('');
80+
81+
browser.driver.executeScript('history.back()');
82+
browser.driver.executeScript('history.forward()');
83+
expect(element(by.css('body')).getAttribute('class')).toBe('');
84+
});
1785
});

0 commit comments

Comments
 (0)