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

Commit 1adf29a

Browse files
committed
fix($compile): sanitize values bound to img[src]
Ref: 9532234 BREAKING CHANGE: img[src] URLs are now sanitized using the same whitelist as a[href] URLs. The most obvious impact is if you were using data: URIs. data: URIs will be whitelisted for img[src] in a future commit.
1 parent 99e85fc commit 1adf29a

File tree

2 files changed

+162
-9
lines changed

2 files changed

+162
-9
lines changed

src/ng/compile.js

+12-8
Original file line numberDiff line numberDiff line change
@@ -215,14 +215,15 @@ function $CompileProvider($provide) {
215215
*
216216
* @description
217217
* Retrieves or overrides the default regular expression that is used for whitelisting of safe
218-
* urls during a[href] sanitization.
218+
* urls during a[href] and img[src] sanitization.
219219
*
220220
* The sanitization is a security measure aimed at prevent XSS attacks via html links.
221221
*
222-
* Any url about to be assigned to a[href] via data-binding is first normalized and turned into an
223-
* absolute url. Afterwards the url is matched against the `urlSanitizationWhitelist` regular
224-
* expression. If a match is found the original url is written into the dom. Otherwise the
225-
* absolute url is prefixed with `'unsafe:'` string and only then it is written into the DOM.
222+
* Any url about to be assigned to a[href] or img[src] via data-binding is first normalized and
223+
* turned into an absolute url. Afterwards, the url is matched against the
224+
* `urlSanitizationWhitelist` regular expression. If a match is found, the original url is written
225+
* into the dom. Otherwise, the absolute url is prefixed with `'unsafe:'` string and only then is
226+
* it written into the DOM.
226227
*
227228
* @param {RegExp=} regexp New regexp to whitelist urls with.
228229
* @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
@@ -264,7 +265,8 @@ function $CompileProvider($provide) {
264265
$set: function(key, value, writeAttr, attrName) {
265266
var booleanKey = getBooleanAttrName(this.$$element[0], key),
266267
$$observers = this.$$observers,
267-
normalizedVal;
268+
normalizedVal,
269+
nodeName;
268270

269271
if (booleanKey) {
270272
this.$$element.prop(key, value);
@@ -284,8 +286,10 @@ function $CompileProvider($provide) {
284286
}
285287

286288

287-
// sanitize a[href] values
288-
if (nodeName_(this.$$element[0]) === 'A' && key === 'href') {
289+
// sanitize a[href] and img[src] values
290+
nodeName = nodeName_(this.$$element);
291+
if ((nodeName === 'A' && key === 'href') ||
292+
(nodeName === 'IMG' && key === 'src')){
289293
urlSanitizationNode.setAttribute('href', value);
290294

291295
// href property always returns normalized absolute url, so we can match against that

test/ng/compileSpec.js

+150-1
Original file line numberDiff line numberDiff line change
@@ -2536,7 +2536,156 @@ describe('$compile', function() {
25362536
});
25372537

25382538

2539-
describe('href sanitization', function() {
2539+
describe('img[src] sanitization', function() {
2540+
it('should NOT require trusted values for img src', inject(function($rootScope, $compile) {
2541+
element = $compile('<img src="{{testUrl}}"></img>')($rootScope);
2542+
$rootScope.testUrl = 'http://example.com/image.png';
2543+
$rootScope.$digest();
2544+
expect(element.attr('src')).toEqual('http://example.com/image.png');
2545+
}));
2546+
2547+
it('should sanitize javascript: urls', inject(function($compile, $rootScope) {
2548+
element = $compile('<img src="{{testUrl}}"></a>')($rootScope);
2549+
$rootScope.testUrl = "javascript:doEvilStuff()";
2550+
$rootScope.$apply();
2551+
expect(element.attr('src')).toBe('unsafe:javascript:doEvilStuff()');
2552+
}));
2553+
2554+
it('should sanitize data: urls', inject(function($compile, $rootScope) {
2555+
element = $compile('<img src="{{testUrl}}"></a>')($rootScope);
2556+
$rootScope.testUrl = "data:evilPayload";
2557+
$rootScope.$apply();
2558+
2559+
expect(element.attr('src')).toBe('unsafe:data:evilPayload');
2560+
}));
2561+
2562+
2563+
it('should sanitize obfuscated javascript: urls', inject(function($compile, $rootScope) {
2564+
element = $compile('<img src="{{testUrl}}"></img>')($rootScope);
2565+
2566+
// case-sensitive
2567+
$rootScope.testUrl = "JaVaScRiPt:doEvilStuff()";
2568+
$rootScope.$apply();
2569+
expect(element[0].src).toBe('unsafe:javascript:doEvilStuff()');
2570+
2571+
// tab in protocol
2572+
$rootScope.testUrl = "java\u0009script:doEvilStuff()";
2573+
$rootScope.$apply();
2574+
expect(element[0].src).toMatch(/(http:\/\/|unsafe:javascript:doEvilStuff\(\))/);
2575+
2576+
// space before
2577+
$rootScope.testUrl = " javascript:doEvilStuff()";
2578+
$rootScope.$apply();
2579+
expect(element[0].src).toBe('unsafe:javascript:doEvilStuff()');
2580+
2581+
// ws chars before
2582+
$rootScope.testUrl = " \u000e javascript:doEvilStuff()";
2583+
$rootScope.$apply();
2584+
expect(element[0].src).toMatch(/(http:\/\/|unsafe:javascript:doEvilStuff\(\))/);
2585+
2586+
// post-fixed with proper url
2587+
$rootScope.testUrl = "javascript:doEvilStuff(); http://make.me/look/good";
2588+
$rootScope.$apply();
2589+
expect(element[0].src).toBeOneOf(
2590+
'unsafe:javascript:doEvilStuff(); http://make.me/look/good',
2591+
'unsafe:javascript:doEvilStuff();%20http://make.me/look/good'
2592+
);
2593+
}));
2594+
2595+
it('should sanitize ng-src bindings as well', inject(function($compile, $rootScope) {
2596+
element = $compile('<img ng-src="{{testUrl}}"></img>')($rootScope);
2597+
$rootScope.testUrl = "javascript:doEvilStuff()";
2598+
$rootScope.$apply();
2599+
2600+
expect(element[0].src).toBe('unsafe:javascript:doEvilStuff()');
2601+
}));
2602+
2603+
2604+
it('should not sanitize valid urls', inject(function($compile, $rootScope) {
2605+
element = $compile('<img src="{{testUrl}}"></img>')($rootScope);
2606+
2607+
$rootScope.testUrl = "foo/bar";
2608+
$rootScope.$apply();
2609+
expect(element.attr('src')).toBe('foo/bar');
2610+
2611+
$rootScope.testUrl = "/foo/bar";
2612+
$rootScope.$apply();
2613+
expect(element.attr('src')).toBe('/foo/bar');
2614+
2615+
$rootScope.testUrl = "../foo/bar";
2616+
$rootScope.$apply();
2617+
expect(element.attr('src')).toBe('../foo/bar');
2618+
2619+
$rootScope.testUrl = "#foo";
2620+
$rootScope.$apply();
2621+
expect(element.attr('src')).toBe('#foo');
2622+
2623+
$rootScope.testUrl = "http://foo/bar";
2624+
$rootScope.$apply();
2625+
expect(element.attr('src')).toBe('http://foo/bar');
2626+
2627+
$rootScope.testUrl = " http://foo/bar";
2628+
$rootScope.$apply();
2629+
expect(element.attr('src')).toBe(' http://foo/bar');
2630+
2631+
$rootScope.testUrl = "https://foo/bar";
2632+
$rootScope.$apply();
2633+
expect(element.attr('src')).toBe('https://foo/bar');
2634+
2635+
$rootScope.testUrl = "ftp://foo/bar";
2636+
$rootScope.$apply();
2637+
expect(element.attr('src')).toBe('ftp://foo/bar');
2638+
2639+
// Fails on IE < 10 with "TypeError: Access is denied" when trying to set img[src]
2640+
if (!msie || msie > 10) {
2641+
$rootScope.testUrl = "mailto:foo@bar.com";
2642+
$rootScope.$apply();
2643+
expect(element.attr('src')).toBe('mailto:foo@bar.com');
2644+
}
2645+
2646+
$rootScope.testUrl = "file:///foo/bar.html";
2647+
$rootScope.$apply();
2648+
expect(element.attr('src')).toBe('file:///foo/bar.html');
2649+
}));
2650+
2651+
2652+
it('should not sanitize attributes other than src', inject(function($compile, $rootScope) {
2653+
element = $compile('<img title="{{testUrl}}"></img>')($rootScope);
2654+
$rootScope.testUrl = "javascript:doEvilStuff()";
2655+
$rootScope.$apply();
2656+
2657+
expect(element.attr('title')).toBe('javascript:doEvilStuff()');
2658+
}));
2659+
2660+
2661+
it('should allow reconfiguration of the src whitelist', function() {
2662+
module(function($compileProvider) {
2663+
expect($compileProvider.urlSanitizationWhitelist() instanceof RegExp).toBe(true);
2664+
var returnVal = $compileProvider.urlSanitizationWhitelist(/javascript:/);
2665+
expect(returnVal).toBe($compileProvider);
2666+
});
2667+
2668+
inject(function($compile, $rootScope) {
2669+
element = $compile('<img src="{{testUrl}}"></img>')($rootScope);
2670+
2671+
// Fails on IE < 10 with "TypeError: Object doesn't support this property or method" when
2672+
// trying to set img[src]
2673+
if (!msie || msie > 10) {
2674+
$rootScope.testUrl = "javascript:doEvilStuff()";
2675+
$rootScope.$apply();
2676+
expect(element.attr('src')).toBe('javascript:doEvilStuff()');
2677+
}
2678+
2679+
$rootScope.testUrl = "http://recon/figured";
2680+
$rootScope.$apply();
2681+
expect(element.attr('src')).toBe('unsafe:http://recon/figured');
2682+
});
2683+
});
2684+
2685+
});
2686+
2687+
2688+
describe('a[href] sanitization', function() {
25402689

25412690
it('should sanitize javascript: urls', inject(function($compile, $rootScope) {
25422691
element = $compile('<a href="{{testUrl}}"></a>')($rootScope);

0 commit comments

Comments
 (0)