-
Notifications
You must be signed in to change notification settings - Fork 27.4k
feat($sanitize): support enhancing white-list #16326
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we are doing this (I am not sure how this would affect security), we need to better document what "valid elements/attributes" are and how they are used by $sanitize
.
src/ngSanitize/sanitize.js
Outdated
|
||
if (angular.isObject(elements)) { | ||
_validElements = []; | ||
angular.forEach(['voidElements', 'inlineElements', 'blockElements', 'svgElements'], function(elementsType) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't take into account whether svgEnabled
is true or false.
src/ngSanitize/sanitize.js
Outdated
* @name $sanitizeProvider#addValidElements | ||
* @kind function | ||
* | ||
* @param {Array|Object=} elements List of valid elements. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the =
? This is not optional afaict.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copy-paste (shame on me)
src/ngSanitize/sanitize.js
Outdated
* @kind function | ||
* | ||
* @param {Array} attrs List of valid attributes. | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be worth documenting that the added attributes will not be treated as URI attributes, i.e. their values will not be sanitized as URIs.
test/ngSanitize/sanitizeSpec.js
Outdated
describe('Custom white-list support', function() { | ||
beforeEach(module(function($sanitizeProvider) { | ||
$sanitizeProvider.addValidElements({ | ||
inlineElements: ['button'], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need tests for all supported categories (e.g. blockElements
, svgElements
).
test/ngSanitize/sanitizeSpec.js
Outdated
@@ -280,6 +280,29 @@ describe('HTML', function() { | |||
expect(doc).toEqual('<p><img src="x"></p>'); | |||
})); | |||
|
|||
describe('Custom white-list support', function() { | |||
beforeEach(module(function($sanitizeProvider) { | |||
$sanitizeProvider.addValidElements({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need a test for passing an array instead of an object.
test/ngSanitize/sanitizeSpec.js
Outdated
expectHTML('<button></button>').toEqual('<button></button>'); | ||
}); | ||
|
||
it('should correct handle custom white-listed void element', function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
correct --> correctly
test/ngSanitize/sanitizeSpec.js
Outdated
$sanitizeProvider.addValidAttrs(['type']); | ||
})); | ||
|
||
it('should not ignore custom white-listed element', function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"should not ignore" is not very straight forward. I would change it to something like "should allow".
test/ngSanitize/sanitizeSpec.js
Outdated
voidElements: ['input'] | ||
}); | ||
|
||
$sanitizeProvider.addValidAttrs(['type']); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
type
is already a valid attribute.
src/ngSanitize/sanitize.js
Outdated
if (elementsType === 'voidElements') { | ||
angular.extend(voidElements, toMap(elements.voidElements.join(','))); | ||
} | ||
_validElements = _validElements.concat(elements[elementsType]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we treat most categories (except for voidElements
and svgElements
) the same, I am wondering if it would be more straight forward to use a simpler public API, e.g.:
{
htmlElements: [...],
htmlVoidElements: [...],
svgElements: [...],
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me
One problem with this is that theortically any 3rd party module can add a config block that changes the whitelist, possibly allowing things you don't want to have in your app. |
@gkalpak any documentation text welcome
@Narretz yours suggestions? |
It's true that the sanitizer can be replaced completely. Maybe we should add a note about the security implications of this, though. |
@gkalpak is it ok now? Thanks |
src/ngSanitize/sanitize.js
Outdated
* @name $sanitizeProvider#addValidAttrs | ||
* @kind function | ||
* | ||
* @param {Array} attrs List of valid attributes. The added attributes will not be treated as URI attributes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would expand a bit on the non-URI part:
[...] The added attributes will not be treated as URI attributes, which means their values will not sanitized as URIs using the
aHrefSanitizationWhitelist
andimgSrcSanitizationWhitelist
of {@link ng.$compileProvider$compileProvider
}.
src/ngSanitize/sanitize.js
Outdated
} | ||
|
||
if (elementsType === 'svgElements') { | ||
angular.extend(svgElements, toMap(elements[elementsType].join(','))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extending svgElements
has no effect if called after the service has been instantiated. This can be unintuitive (because theoretically it is possible to use provider methods after a service has been instantiated and in many cases everything works as expected).
I think it would be better to either throw or make it a no-op after instantiation (and document that).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I see enableSvg
also doesn't work after instantiation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be better to either throw or make it a no-op after instantiation (and document that).
How can I check that service has been instantiated? (without additional variable)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can't (without additional variable) 😁
test/ngSanitize/sanitizeSpec.js
Outdated
@@ -280,10 +280,51 @@ describe('HTML', function() { | |||
expect(doc).toEqual('<p><img src="x"></p>'); | |||
})); | |||
|
|||
describe('Custom white-list support', function() { | |||
beforeEach(module(function($sanitizeProvider) { | |||
$sanitizeProvider.addValidElements(['form']); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In order to make sure this test does indeed test what it is supposed to (now and in the future), it might be better to add arbitrary elements (that do not exist in the HTML5 spec) to ensure that they are not already considered valid.
test/ngSanitize/sanitizeSpec.js
Outdated
})); | ||
|
||
it('should allow custom white-listed element', function() { | ||
expectHTML('<form></form>').toEqual('<form></form>'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be a test that this is not allowed as voidElement
.
test/ngSanitize/sanitizeSpec.js
Outdated
}); | ||
|
||
it('should allow custom white-listed void element', function() { | ||
expectHTML('<input/>').toEqual('<input>'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be a test that this is also allowed as non-void element.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have added check
expectHTML('<input/>').not.toEqual('<input></input>');
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I meant something like expectHTML('<input></input>')
(which is accepted atm).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔 I didn't get your point.
There should be a test that this is also allowed as non-void element.
We already have such test
https://github.com/angular/angular.js/pull/16326/files#diff-85da78d5073ece90913c9e733cfcdef4R306
Yes, the test above not about input
but I don't think that input
tag is something special.
I have renamed input
to foo-input
to emphasize that name of the tag isn't matter
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That test is for a custom non-void element.
There should be a test that custom void element can be used as <foo-input></foo-input>
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good. We're getting close 😃
src/ngSanitize/sanitize.js
Outdated
* @kind function | ||
* | ||
* @param {Array|Object} elements List of valid elements. | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We need more docs (and also mention that it has no effect after the service has been instantiated).
@maksimr, can you take a first stab at documenting what this method does and "valid elements/attributes" are and how they are used by $sanitize
?
src/ngSanitize/sanitize.js
Outdated
* @name $sanitizeProvider#addValidElements | ||
* @kind function | ||
* | ||
* @param {Array|Object} elements List of valid elements. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should mention the "shape" of the Object
version.
src/ngSanitize/sanitize.js
Outdated
addElementsTo(validElements, elements['htmlElements']); | ||
} | ||
|
||
function addElementsTo(elementsMap, newElements) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no reason for this function to be inside the closure. Move it outside to avoid re-creating it on every invokation.
src/ngSanitize/sanitize.js
Outdated
|
||
function addElementsTo(elementsMap, newElements) { | ||
if (newElements && newElements.length) { | ||
return angular.extend(elementsMap, toMap(newElements.join(','))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The return value is ignored. You can drop it.
It also seems redundant to join()
items only to split them right away (inside toMap()
). Can you refactor toMap()
to avoid that? Something like:
+function stringToMap(str, lowercaseKeys) {
+ return arrayToMap(str.split(','), lowercaseKeys);
+}
-function toMap(str, lowercaseKeys) {
- var obj = {}, items = str.split(','), i;
+function arrayToMap(items, lowercaseKeys) {
+ var obj = {}, i;
for (i = 0; i < items.length; i++) {
obj[lowercaseKeys ? lowercase(items[i]) : items[i]] = true;
}
return obj;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, use the extend
function from the closure here and elsewhere (see below the Private stuff comment).
Do the same for other helpers (isArray
, isObject
). (Define the ones that are not defined already in "Private stuff".)
src/ngSanitize/sanitize.js
Outdated
* | ||
* @param {Array|Object} elements List of valid elements. | ||
*/ | ||
this.addValidElements = function(elements) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's have this method return this
to be inline with what we do with other setter methods (helps with chaining).
src/ngSanitize/sanitize.js
Outdated
* | ||
* @param {Array} attrs List of valid attributes The added attributes will not be treated as URI attributes, which means their values will | ||
* not sanitized as URIs using the aHrefSanitizationWhitelist and imgSrcSanitizationWhitelist of {@link ng.$compileProvider $compileProvider}. | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs more docs as well. You could also move most of the param description into the main description of the function.
test/ngSanitize/sanitizeSpec.js
Outdated
|
||
it('should not allow add custom element after service has been instantiated', inject(function($sanitize) { | ||
$sanitizeProvider.addValidElements(['bar']); | ||
expect($sanitize).toBeDefined(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assertion is unnecessary.
test/ngSanitize/sanitizeSpec.js
Outdated
}); | ||
|
||
it('should allow custom white-listed block element', function() { | ||
expectHTML('<foo-video></foo-video>').toEqual('<foo-video></foo-video>'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not a block
element. Let's merge into the tests above.
test/ngSanitize/sanitizeSpec.js
Outdated
expectHTML('<foo></foo>').toEqual('<foo></foo>'); | ||
}); | ||
|
||
it('should allow custom white-listed inline element', function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not necessarily a inline
element. Just call it element
😃
And merge it with the above test.
test/ngSanitize/sanitizeSpec.js
Outdated
expectHTML('<foo-input/>').toEqual('<foo-input>'); | ||
}); | ||
|
||
it('should correct handle void element', function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's change this to:
should allow custom white-listed void element to be used with closing tag
@gkalpak could you provide documentation for these methods |
@gkalpak ping |
So there's good news and bad news. 👍 The good news is that everyone that needs to sign a CLA (the pull request submitter and all commit authors) have done so. Everything is all good there. 😕 The bad news is that it appears that one or more commits were authored by someone other than the pull request submitter. We need to confirm that all authors are ok with their commits being contributed to this project. Please have them confirm that here in the pull request. Note to project maintainer: This is a terminal state, meaning the |
1 similar comment
So there's good news and bad news. 👍 The good news is that everyone that needs to sign a CLA (the pull request submitter and all commit authors) have done so. Everything is all good there. 😕 The bad news is that it appears that one or more commits were authored by someone other than the pull request submitter. We need to confirm that all authors are ok with their commits being contributed to this project. Please have them confirm that here in the pull request. Note to project maintainer: This is a terminal state, meaning the |
I've rebased this on master, made some small changes and added docs. |
@@ -207,6 +299,8 @@ function $SanitizeProvider() { | |||
extend = angular.extend; | |||
forEach = angular.forEach; | |||
isDefined = angular.isDefined; | |||
isArray = angular.isArray; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no alphabetic ordering??
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't see any signs of "alphabetic ordering" 🤔
Should I change this code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The var
declarations at the top of the file are listed in alphabetical order; and @gkalpak is usually a stickler for this kind of thing :-)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@petebacondarwin looks like @gkalpak fixed it already
1920420#diff-699beb0d8c48a8257d3d70331bf6138dR19
@gkalpak Thanks
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ts, ts, ts! How could you think I wouldn't have fixed that, @petebacondarwin 😛
😱 😱 😱 😱 😱 I fixed it in one of two places 😱 😱 😱 😱 😱
Thx, @petebacondarwin ❤️ (Sorry I doubted you 😞)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed while merging (and also removed unused isObject
var).
}); | ||
|
||
it('should allow custom white-listed void element', function() { | ||
expectHTML('<foo-input/>').toEqual('<foo-input>'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if the element passed in is not empty? E.g. <foo-input>some content</foo-input>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sanitizer removes closing tag
<foo-input>some content
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is interesting...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yup, but this is how current sanitizer works.
When it meets token <foo-inpupt>
it treats this as void element because it's valid case when self-closing element(void) does not have /
, after that it meets text some content
and we do nothing and finally we meet </foo-input>
and because there are no open element before we just remove it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will make it more explicit in the docs that void elements cannot have content.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cannot or should not?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cannot and should not 😁
As @maksimr mentioned $sanitize
(and browsers) will transform <void-element>foo</void-element>
to <void-element>foo
(so the content ends up outside the element).
}); | ||
|
||
it('should allow custom white-listed attribute', function() { | ||
expectHTML('<foo-input foo="foo"/>').toEqual('<foo-input foo="foo">'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any difference if the attribute is on a blacklisted element?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sanitizer just remove a blacklisted element
src/ngSanitize/sanitize.js
Outdated
* | ||
* - `svgElements`: This is similar to `htmlElements`, but for SVG elements. This list is only | ||
* taken into account if SVG is {@link ngSanitize.$sanitizeProvider#enableSvg enabled} for | ||
* `$sanitize`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it is not possible to have "void svg" elements?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes and No, because in sanitizer no type like "void svg" I didn't add such property, but if you know how works sanitizer you could do something like this:
$sanitizeProvider.addValidElements({
htmlVoidElements: ['font-face-uri'],
svgElements: ['font-face-uri']
});
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM - a couple of unimportant questions.
So we don't need an extra notice that this method should be used carefully, because adding more elements can jeopardize the security of the app? |
I assume the purpose of this feature is to support HTML5 custom web elements? |
Not only, in our case we want support for checkboxes. |
It can also be used to add elements / attributes where we haven't decided if they should be whitelisted: #13282 |
A good thing is that you can't "overwrite" the
@maksimr, can you elaborate? What do you mean by "checkboxes"? |
@gkalpak sure, "checkboxes" is |
Oh, OK. With this change you will have to whitelist all |
Updated the docs. Will merge once Travis is green. |
#5900 fix
What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)
Improve functionality of ngSanitize. More information you could find here - #5900
What is the current behavior? (You can also link to an open issue here)
Hardcoded white-list
What is the new behavior (if this is a feature change)?
Allow adding valid elements and attributes
Does this PR introduce a breaking change?
No
Please check if the PR fulfills these requirements
Other information: