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

feat($sanitize): support enhancing white-list #16326

Closed
wants to merge 3 commits into from
Closed

feat($sanitize): support enhancing white-list #16326

wants to merge 3 commits into from

Conversation

maksimr
Copy link
Contributor

@maksimr maksimr commented Nov 11, 2017

#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

  • The commit message follows our guidelines
  • Fix/Feature: Docs have been added/updated
  • Fix/Feature: Tests have been added; existing tests pass

Other information:

Copy link
Member

@gkalpak gkalpak left a 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.


if (angular.isObject(elements)) {
_validElements = [];
angular.forEach(['voidElements', 'inlineElements', 'blockElements', 'svgElements'], function(elementsType) {
Copy link
Member

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.

* @name $sanitizeProvider#addValidElements
* @kind function
*
* @param {Array|Object=} elements List of valid elements.
Copy link
Member

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.

Copy link
Contributor Author

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)

* @kind function
*
* @param {Array} attrs List of valid attributes.
*/
Copy link
Member

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.

describe('Custom white-list support', function() {
beforeEach(module(function($sanitizeProvider) {
$sanitizeProvider.addValidElements({
inlineElements: ['button'],
Copy link
Member

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).

@@ -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({
Copy link
Member

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.

expectHTML('<button></button>').toEqual('<button></button>');
});

it('should correct handle custom white-listed void element', function() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correct --> correctly

$sanitizeProvider.addValidAttrs(['type']);
}));

it('should not ignore custom white-listed element', function() {
Copy link
Member

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".

voidElements: ['input']
});

$sanitizeProvider.addValidAttrs(['type']);
Copy link
Member

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.

if (elementsType === 'voidElements') {
angular.extend(voidElements, toMap(elements.voidElements.join(',')));
}
_validElements = _validElements.concat(elements[elementsType]);
Copy link
Member

@gkalpak gkalpak Nov 13, 2017

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: [...],
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me

@Narretz
Copy link
Contributor

Narretz commented Nov 13, 2017

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.

@maksimr
Copy link
Contributor Author

maksimr commented Nov 13, 2017

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

@gkalpak any documentation text welcome

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.

@Narretz yours suggestions?
Theoretically, any 3rd party module can replace $sanitize service

@Narretz
Copy link
Contributor

Narretz commented Nov 15, 2017

It's true that the sanitizer can be replaced completely. Maybe we should add a note about the security implications of this, though.

@maksimr
Copy link
Contributor Author

maksimr commented Nov 16, 2017

@gkalpak is it ok now?

Thanks

* @name $sanitizeProvider#addValidAttrs
* @kind function
*
* @param {Array} attrs List of valid attributes. The added attributes will not be treated as URI attributes
Copy link
Member

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 and imgSrcSanitizationWhitelist of {@link ng.$compileProvider $compileProvider}.

}

if (elementsType === 'svgElements') {
angular.extend(svgElements, toMap(elements[elementsType].join(',')));
Copy link
Member

@gkalpak gkalpak Nov 21, 2017

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).

Copy link
Contributor Author

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gkalpak

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)

Copy link
Member

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) 😁

@@ -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']);
Copy link
Member

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.

}));

it('should allow custom white-listed element', function() {
expectHTML('<form></form>').toEqual('<form></form>');
Copy link
Member

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.

});

it('should allow custom white-listed void element', function() {
expectHTML('<input/>').toEqual('<input>');
Copy link
Member

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.

Copy link
Contributor Author

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>');

Copy link
Member

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).

Copy link
Contributor Author

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

Copy link
Member

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>.

Copy link
Contributor Author

@maksimr maksimr Dec 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

@gkalpak gkalpak left a 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 😃

* @kind function
*
* @param {Array|Object} elements List of valid elements.
*/
Copy link
Member

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?

* @name $sanitizeProvider#addValidElements
* @kind function
*
* @param {Array|Object} elements List of valid elements.
Copy link
Member

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.

addElementsTo(validElements, elements['htmlElements']);
}

function addElementsTo(elementsMap, newElements) {
Copy link
Member

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.


function addElementsTo(elementsMap, newElements) {
if (newElements && newElements.length) {
return angular.extend(elementsMap, toMap(newElements.join(',')));
Copy link
Member

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;
 }

Copy link
Member

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".)

*
* @param {Array|Object} elements List of valid elements.
*/
this.addValidElements = function(elements) {
Copy link
Member

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).

*
* @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}.
*/
Copy link
Member

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.


it('should not allow add custom element after service has been instantiated', inject(function($sanitize) {
$sanitizeProvider.addValidElements(['bar']);
expect($sanitize).toBeDefined();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assertion is unnecessary.

});

it('should allow custom white-listed block element', function() {
expectHTML('<foo-video></foo-video>').toEqual('<foo-video></foo-video>');
Copy link
Member

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.

expectHTML('<foo></foo>').toEqual('<foo></foo>');
});

it('should allow custom white-listed inline element', function() {
Copy link
Member

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.

expectHTML('<foo-input/>').toEqual('<foo-input>');
});

it('should correct handle void element', function() {
Copy link
Member

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

@maksimr
Copy link
Contributor Author

maksimr commented Dec 7, 2017

@gkalpak could you provide documentation for these methods

#16326 (comment)
#16326 (comment)

@maksimr
Copy link
Contributor Author

maksimr commented Dec 21, 2017

@gkalpak ping

@gkalpak gkalpak self-assigned this Feb 2, 2018
@googlebot
Copy link

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 cla/google commit status will not change from this State. It's up to you to confirm consent of the commit author(s) and merge this pull request when appropriate.

1 similar comment
@googlebot
Copy link

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 cla/google commit status will not change from this State. It's up to you to confirm consent of the commit author(s) and merge this pull request when appropriate.

@googlebot googlebot added cla: no and removed cla: yes labels Feb 5, 2018
@gkalpak
Copy link
Member

gkalpak commented Feb 5, 2018

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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no alphabetic ordering??

Copy link
Contributor Author

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?

Copy link
Contributor

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 :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

@gkalpak gkalpak Feb 6, 2018

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 😞)

Copy link
Member

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>');
Copy link
Contributor

@petebacondarwin petebacondarwin Feb 5, 2018

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>

Copy link
Contributor Author

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

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is interesting...

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cannot or should not?

Copy link
Member

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">');
Copy link
Contributor

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?

Copy link
Contributor Author

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

*
* - `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`.
Copy link
Contributor

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?

Copy link
Contributor Author

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']
      });

Copy link
Contributor

@petebacondarwin petebacondarwin left a 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.

@Narretz
Copy link
Contributor

Narretz commented Feb 5, 2018

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?

@petebacondarwin
Copy link
Contributor

I assume the purpose of this feature is to support HTML5 custom web elements?

@maksimr
Copy link
Contributor Author

maksimr commented Feb 5, 2018

I assume the purpose of this feature is to support HTML5 custom web elements?

Not only, in our case we want support for checkboxes.

@Narretz
Copy link
Contributor

Narretz commented Feb 5, 2018

It can also be used to add elements / attributes where we haven't decided if they should be whitelisted: #13282

@gkalpak
Copy link
Member

gkalpak commented Feb 6, 2018

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?

A good thing is that you can't "overwrite" the blockedElements list. So, even adding script or style (which are currently blocked) will not make them "safe". But you can add other elements and you can definitely add attributes.
I'll add a note to the docs.

Not only, in our case we want support for checkboxes.

@maksimr, can you elaborate? What do you mean by "checkboxes"?

@maksimr
Copy link
Contributor Author

maksimr commented Feb 6, 2018

@gkalpak sure,

"checkboxes" is <input type=checkbox/>

@gkalpak
Copy link
Member

gkalpak commented Feb 6, 2018

Oh, OK. With this change you will have to whitelist all input elements (so be careful 😃).

@gkalpak
Copy link
Member

gkalpak commented Feb 6, 2018

Updated the docs. Will merge once Travis is green.
(If you want to TAL, now is your chance, @petebacondarwin @Narretz 😃)

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants