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

Commit 93ce592

Browse files
committed
feat($sce): simpler patterns for $sceDelegateProviders white/blacklists
Closes #4006
1 parent 1f686c4 commit 93ce592

File tree

4 files changed

+300
-49
lines changed

4 files changed

+300
-49
lines changed

docs/content/error/sce/imatcher.ngdoc

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
@ngdoc error
2+
@name $sce:imatcher
3+
@fullName Invalid matcher (only string patterns and RegExp instances are supported)
4+
@description
5+
6+
Please see {@link api/ng.$sceDelegateProvider#resourceUrlWhitelist
7+
$sceDelegateProvider.resourceUrlWhitelist} and {@link
8+
api/ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} for the
9+
list of acceptable items.

docs/content/error/sce/iwcard.ngdoc

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
@ngdoc error
2+
@name $sce:iwcard
3+
@fullName The sequence *** is not a valid pattern wildcard
4+
@description
5+
6+
The strings in {@link api/ng.$sceDelegateProvider#resourceUrlWhitelist
7+
$sceDelegateProvider.resourceUrlWhitelist} and {@link
8+
api/ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist} may not
9+
contain the undefined sequence `***`. Only `*` and `**` wildcard patterns are defined.

src/ng/sce.js

+138-24
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,55 @@ var SCE_CONTEXTS = {
1212
JS: 'js'
1313
};
1414

15+
// Helper functions follow.
16+
17+
// Copied from:
18+
// http://docs.closure-library.googlecode.com/git/closure_goog_string_string.js.source.html#line962
19+
// Prereq: s is a string.
20+
function escapeForRegexp(s) {
21+
return s.replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, '\\$1').
22+
replace(/\x08/g, '\\x08');
23+
};
24+
25+
26+
function adjustMatcher(matcher) {
27+
if (matcher === 'self') {
28+
return matcher;
29+
} else if (isString(matcher)) {
30+
// Strings match exactly except for 2 wildcards - '*' and '**'.
31+
// '*' matches any character except those from the set ':/.?&'.
32+
// '**' matches any character (like .* in a RegExp).
33+
// More than 2 *'s raises an error as it's ill defined.
34+
if (matcher.indexOf('***') > -1) {
35+
throw $sceMinErr('iwcard',
36+
'Illegal sequence *** in string matcher. String: {0}', matcher);
37+
}
38+
matcher = escapeForRegexp(matcher).
39+
replace('\\*\\*', '.*').
40+
replace('\\*', '[^:/.?&;]*');
41+
return new RegExp('^' + matcher + '$');
42+
} else if (isRegExp(matcher)) {
43+
// The only other type of matcher allowed is a Regexp.
44+
// Match entire URL / disallow partial matches.
45+
// Flags are reset (i.e. no global, ignoreCase or multiline)
46+
return new RegExp('^' + matcher.source + '$');
47+
} else {
48+
throw $sceMinErr('imatcher',
49+
'Matchers may only be "self", string patterns or RegExp objects');
50+
}
51+
}
52+
53+
54+
function adjustMatchers(matchers) {
55+
var adjustedMatchers = [];
56+
if (isDefined(matchers)) {
57+
forEach(matchers, function(matcher) {
58+
adjustedMatchers.push(adjustMatcher(matcher));
59+
});
60+
}
61+
return adjustedMatchers;
62+
}
63+
1564

1665
/**
1766
* @ngdoc service
@@ -45,13 +94,37 @@ var SCE_CONTEXTS = {
4594
* @name ng.$sceDelegateProvider
4695
* @description
4796
*
48-
* The $sceDelegateProvider provider allows developers to configure the {@link ng.$sceDelegate
97+
* The `$sceDelegateProvider` provider allows developers to configure the {@link ng.$sceDelegate
4998
* $sceDelegate} service. This allows one to get/set the whitelists and blacklists used to ensure
50-
* that URLs used for sourcing Angular templates are safe. Refer {@link
99+
* that the URLs used for sourcing Angular templates are safe. Refer {@link
51100
* ng.$sceDelegateProvider#resourceUrlWhitelist $sceDelegateProvider.resourceUrlWhitelist} and
52101
* {@link ng.$sceDelegateProvider#resourceUrlBlacklist $sceDelegateProvider.resourceUrlBlacklist}
53102
*
54-
* Read more about {@link ng.$sce Strict Contextual Escaping (SCE)}.
103+
* For the general details about this service in Angular, read the main page for {@link ng.$sce
104+
* Strict Contextual Escaping (SCE)}.
105+
*
106+
* **Example**: Consider the following case. <a name="example"></a>
107+
*
108+
* - your app is hosted at url `http://myapp.example.com/`
109+
* - but some of your templates are hosted on other domains you control such as
110+
* `http://srv01.assets.example.com/`,  `http://srv02.assets.example.com/`, etc.
111+
* - and you have an open redirect at `http://myapp.example.com/clickThru?...`.
112+
*
113+
* Here is what a secure configuration for this scenario might look like:
114+
*
115+
* <pre class="prettyprint">
116+
* angular.module('myApp', []).config(function($sceDelegateProvider) {
117+
* $sceDelegateProvider.resourceUrlWhitelist([
118+
* // Allow same origin resource loads.
119+
* 'self',
120+
* // Allow loading from our assets domain. Notice the difference between * and **.
121+
* 'http://srv*.assets.example.com/**']);
122+
*
123+
* // The blacklist overrides the whitelist so the open redirect here is blocked.
124+
* $sceDelegateProvider.resourceUrlBlacklist([
125+
* 'http://myapp.example.com/clickThru**']);
126+
* });
127+
* </pre>
55128
*/
56129

57130
function $SceDelegateProvider() {
@@ -68,28 +141,25 @@ function $SceDelegateProvider() {
68141
* @function
69142
*
70143
* @param {Array=} whitelist When provided, replaces the resourceUrlWhitelist with the value
71-
* provided. This must be an array.
72-
*
73-
* Each element of this array must either be a regex or the special string `'self'`.
74-
*
75-
* When a regex is used, it is matched against the normalized / absolute URL of the resource
76-
* being tested.
144+
* provided. This must be an array or null. A snapshot of this array is used so further
145+
* changes to the array are ignored.
77146
*
78-
* The **special string** `'self'` can be used to match against all URLs of the same domain as the
79-
* application document with the same protocol (allows sourcing https resources from http documents.)
147+
* Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items allowed in
148+
* this array.
80149
*
81-
* Please note that **an empty whitelist array will block all URLs**!
150+
* Note: **an empty whitelist array will block all URLs**!
82151
*
83152
* @return {Array} the currently set whitelist array.
84153
*
85-
* The **default value** when no whitelist has been explicitly set is `['self']`.
154+
* The **default value** when no whitelist has been explicitly set is `['self']` allowing only
155+
* same origin resource requests.
86156
*
87157
* @description
88158
* Sets/Gets the whitelist of trusted resource URLs.
89159
*/
90160
this.resourceUrlWhitelist = function (value) {
91161
if (arguments.length) {
92-
resourceUrlWhitelist = value;
162+
resourceUrlWhitelist = adjustMatchers(value);
93163
}
94164
return resourceUrlWhitelist;
95165
};
@@ -101,13 +171,11 @@ function $SceDelegateProvider() {
101171
* @function
102172
*
103173
* @param {Array=} blacklist When provided, replaces the resourceUrlBlacklist with the value
104-
* provided. This must be an array.
174+
* provided. This must be an array or null. A snapshot of this array is used so further
175+
* changes to the array are ignored.
105176
*
106-
* Each element of this array must either be a regex or the special string `'self'` (see
107-
* `resourceUrlWhitelist` for meaning - it's only really useful there.)
108-
*
109-
* When a regex is used, it is matched against the normalized / absolute URL of the resource
110-
* being tested.
177+
* Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items allowed in
178+
* this array.
111179
*
112180
* The typical usage for the blacklist is to **block [open redirects](http://cwe.mitre.org/data/definitions/601.html)**
113181
* served by your domain as these would otherwise be trusted but actually return content from the redirected
@@ -126,7 +194,7 @@ function $SceDelegateProvider() {
126194

127195
this.resourceUrlBlacklist = function (value) {
128196
if (arguments.length) {
129-
resourceUrlBlacklist = value;
197+
resourceUrlBlacklist = adjustMatchers(value);
130198
}
131199
return resourceUrlBlacklist;
132200
};
@@ -147,7 +215,8 @@ function $SceDelegateProvider() {
147215
if (matcher === 'self') {
148216
return $$urlUtils.isSameOrigin(parsedUrl);
149217
} else {
150-
return !!parsedUrl.href.match(matcher);
218+
// definitely a regex. See adjustMatchers()
219+
return !!matcher.exec(parsedUrl.href);
151220
}
152221
}
153222

@@ -457,9 +526,54 @@ function $SceDelegateProvider() {
457526
* | `$sce.RESOURCE_URL` | For URLs that are not only safe to follow as links, but whose contens are also safe to include in your application. Examples include `ng-include`, `src` / `ngSrc` bindings for tags other than `IMG` (e.g. `IFRAME`, `OBJECT`, etc.) <br><br>Note that `$sce.RESOURCE_URL` makes a stronger statement about the URL than `$sce.URL` does and therefore contexts requiring values trusted for `$sce.RESOURCE_URL` can be used anywhere that values trusted for `$sce.URL` are required. |
458527
* | `$sce.JS` | For JavaScript that is safe to execute in your application's context. Currently unused. Feel free to use it in your own directives. |
459528
*
460-
* ## Show me an example.
529+
* ## Format of items in {@link ng.$sceDelegateProvider#resourceUrlWhitelist resourceUrlWhitelist}/{@link ng.$sceDelegateProvider#resourceUrlBlacklist Blacklist} <a name="resourceUrlPatternItem"></a>
530+
*
531+
* Each element in these arrays must be one of the following:
532+
*
533+
* - **'self'**
534+
* - The special **string**, `'self'`, can be used to match against all URLs of the **same
535+
* domain** as the application document using the **same protocol**.
536+
* - **String** (except the special value `'self'`)
537+
* - The string is matched against the full *normalized / absolute URL* of the resource
538+
* being tested (substring matches are not good enough.)
539+
* - There are exactly **two wildcard sequences** - `*` and `**`. All other characters
540+
* match themselves.
541+
* - `*`: matches zero or more occurances of any character other than one of the following 6
542+
* characters: '`:`', '`/`', '`.`', '`?`', '`&`' and ';'. It's a useful wildcard for use
543+
* in a whitelist.
544+
* - `**`: matches zero or more occurances of *any* character. As such, it's not
545+
* not appropriate to use in for a scheme, domain, etc. as it would match too much. (e.g.
546+
* http://**.example.com/ would match http://evil.com/?ignore=.example.com/ and that might
547+
* not have been the intention.) It's usage at the very end of the path is ok. (e.g.
548+
* http://foo.example.com/templates/**).
549+
* - **RegExp** (*see caveat below*)
550+
* - *Caveat*: While regular expressions are powerful and offer great flexibility, their syntax
551+
* (and all the inevitable escaping) makes them *harder to maintain*. It's easy to
552+
* accidentally introduce a bug when one updates a complex expression (imho, all regexes should
553+
* have good test coverage.). For instance, the use of `.` in the regex is correct only in a
554+
* small number of cases. A `.` character in the regex used when matching the scheme or a
555+
* subdomain could be matched against a `:` or literal `.` that was likely not intended. It
556+
* is highly recommended to use the string patterns and only fall back to regular expressions
557+
* if they as a last resort.
558+
* - The regular expression must be an instance of RegExp (i.e. not a string.) It is
559+
* matched against the **entire** *normalized / absolute URL* of the resource being tested
560+
* (even when the RegExp did not have the `^` and `$` codes.) In addition, any flags
561+
* present on the RegExp (such as multiline, global, ignoreCase) are ignored.
562+
* - If you are generating your Javascript from some other templating engine (not
563+
* recommended, e.g. in issue [#4006](https://github.com/angular/angular.js/issues/4006)),
564+
* remember to escape your regular expression (and be aware that you might need more than
565+
* one level of escaping depending on your templating engine and the way you interpolated
566+
* the value.) Do make use of your platform's escaping mechanism as it might be good
567+
* enough before coding your own. e.g. Ruby has
568+
* [Regexp.escape(str)](http://www.ruby-doc.org/core-2.0.0/Regexp.html#method-c-escape)
569+
* and Python has [re.escape](http://docs.python.org/library/re.html#re.escape).
570+
* Javascript lacks a similar built in function for escaping. Take a look at Google
571+
* Closure library's [goog.string.regExpEscape(s)](
572+
* http://docs.closure-library.googlecode.com/git/closure_goog_string_string.js.source.html#line962).
461573
*
574+
* Refer {@link ng.$sceDelegateProvider#example $sceDelegateProvider} for an example.
462575
*
576+
* ## Show me an example using SCE.
463577
*
464578
* @example
465579
<example module="mySceApp">
@@ -925,7 +1039,7 @@ function $SceProvider() {
9251039
getTrusted = sce.getTrusted,
9261040
trustAs = sce.trustAs;
9271041

928-
angular.forEach(SCE_CONTEXTS, function (enumValue, name) {
1042+
forEach(SCE_CONTEXTS, function (enumValue, name) {
9291043
var lName = lowercase(name);
9301044
sce[camelCase("parse_as_" + lName)] = function (expr) {
9311045
return parse(enumValue, expr);

0 commit comments

Comments
 (0)