-
Notifications
You must be signed in to change notification settings - Fork 27.5k
fix(jqLite): prevent possible XSS due to regex-based HTML replacement #17028
Conversation
// the select element. | ||
if (msie < 10) { | ||
wrapMap.optgroup = wrapMap.option = [1, '<select multiple="multiple">', '</select>']; | ||
} |
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'm not sure if you actually need the wrapping at all in modern browsers. At least in Chrome appending the elements directly works, even if, say, td
is not under tbody
or table
.
In any case, while the current patch fixes the security bug we currently know about, a better way would be to avoid concatenating HTML strings whatsoever. If, for example, you need wrapping, use document.createElement
and appendChild
and append to the newly created one.
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'm not sure if you actually need the wrapping at all in modern browsers.
You do. See https://jsbin.com/cibiwet/edit?html,js,console, it doesn't actually append the element, either in Firefox or in Chrome.
I'm curious how it worked for you, my test case is pretty basic.
If, for example, you need wrapping, use
document.createElement
andappendChild
and append to the newly created one.
That would work.
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.
Heh, my bad. I would've sworn this worked last week (I was debugging jqLite, so all calls were inside a fragment too), but indeed it does not (only <option>
works), sorry for the confusion.
window.setTimeout(function() { | ||
expect(window.xss).not.toHaveBeenCalledWith(index); | ||
donePartial(); | ||
}, 1000); |
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 it possible to get a false-positive (i.e. an anti-flake)?
If the timeout completes before the async changes have occurred, it is possible that the test could have failed if the timeout were longer.
I guess that the onerror
handler is always called async?
Could we avoid relying upon the timeout being long enough. Perhaps create an additional "real" error that will call a different onerror
handler? Then assume that if this second handler is called but the xss
one is not called then we are good... Then we could make the it
async and only call done()
once the second real error handler has been called.
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.
Feel free to experiment. I've tried a few things when I prepared a similar patch for jQuery & I wasn't able to achieve something that would not have the delay and that wouldn't suffer from race conditions. Maybe there's something but it might require extensive testing - we definitely don't want to miss the error just because it fired too late.
src/jqLite.js
Outdated
@@ -215,7 +229,10 @@ function jqLiteBuildFragment(html, context) { | |||
tmp = fragment.appendChild(context.createElement('div')); | |||
tag = (TAG_NAME_REGEXP.exec(html) || ['', ''])[1].toLowerCase(); | |||
wrap = wrapMap[tag] || wrapMap._default; |
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 could get rid of _default
, e.g. like this:
wrap = wrapMap[tag]
finalHTML = ...
tmp.innerHTML = wrap ? wrap[1] + finalHtml + wrap[2] : finalHtml;
i = wrap ? wrap[0] : 0;
while (i--) { //...
That's a no-op for the security fix, but is shorter - and lets you pipe through the data without any string concatenation (e.g. letting through TrustedHTML
objects that DOMPurify might return).
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.
Sound sensible. That said, this would technically be a new feature and we're at the point where we mostly care about security fixes so I wouldn't want to spend too much time on tweaking 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.
OK, apparently we have a few days to finish the PR; in that case, I can have a look early next week.
'<noscript/><img src=url404 onerror=xss(10)>', | ||
'<noembed><noembed/><img src=url404 onerror=xss(11)>', | ||
|
||
'<option><style></option></select><img src=url404 onerror=xss(12)></style>' |
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 that the following test cases were useful for jQuery (which used a slightly different regex) but not for jqLite's XHTML_TAG_REGEXP (i.e. they would pass without the changes in this PR, so they don't add much value):
'<img alt="<x" title="/><img src=url404 onerror=xss(0)>">'
'<img alt="\n<x" title="/>\n<img src=url404 onerror=xss(1)>">'
'<foo" alt="" title="/><img src=url404 onerror=xss(8)>">'
'<img alt="<x" title="" src="/><img src=url404 onerror=xss(9)>">'
(That is because jQuery's tegex would match "
as part of the tag name, while jqLite won't.)
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’re testing Angular with jQuery as well here. I’d leave them all; the purpose is not to match only what was broken but to prevent future regressions here.
0b474b3
to
f21f2d8
Compare
@koto I refactored the whole thing to not rely on passing strings to This makes the PR increase the minified size by Please take a look. |
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.
On master the angular.min.js
file is 176333 bytes.
This PR appears to make it 176724 (391 byte increase over master).
My two suggestions appear to make it 176544 (211 byte increase over master).
This also splits the wrapping logic to one for modern browsers & one for IE 9 as IE 9 has restrictions that make it impossible to make it as secure.
@petebacondarwin PR updated. |
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 should see if @koto has any more feedback before we merge, yes?
It looks good (thanks!), but I have a small suggestion (and I realise this is not about fixing the bug, but rather adding support for passing You don't modify the payload in How about changing it to : if (argIsString || (typeof TrustedHTML == "function" && element instanceof TrustedHTML)) {
jqLiteAddNodes(this, jqLiteParseHTML(element));
} // ... that would pipe the TrustedHTML objects to For testing, one can create TrustedHTML objects like so: if (window.trustedTypes) {
var policy = trustedTypes.createPolicy('jqlitetests', {createHTML: function(s) { return s }});
var trustedHTML = policy.createHTML('<div><img src=x></div>');
} |
This won't work cross-frame, will it? I'm not sure if that's an important or even supported use case for AngularJS, I know this would be a big roadblock for jQuery. In any case, this is getting a bit far from the original purpose of this PR. I may not have much more time to spend on this PR and if we want to add explicit support for trusted types, we should carefully look at all existing jqLite APIs like A small offtop: as I'm involved in jQuery as well, I've been thinking about including similar improvements to jQuery. AngularJS has included code specific to APIs not available cross-browser like Chrome apps and now possibly trusted types but jQuery has historically avoided such things. Avoiding |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Co-authored-by: Michael Prentice <splaktar@gmail.com>
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
@koto - thanks for your review. Regarding the suggestion of handling passing As I understand it, the suggestion is not an essential fix to help with security, right? If so, then we should leave the PR as it is here, get it merged and release AngularJS to mitigate the current security concern. Does that sounds reasonable or am I missing something? |
Correct. Trusted Types don't work if passed to different realms (i.e. DOM would reject them if Types are enforced), so it makes sense that TT would not be recognised as special values in jqLite as well (
I'd like to understand this more. jQuery would be a TT consumer, the intention of a potential PR is not to make all applications using jQuery compliant with TT restrictions, but only to make apps that already create types able to use jQuery without losing them. Some refactoring is needed at the application side (to create types in the first place, but also to possibly change insecure patterns) But this is off-topic here, let's discuss in jQuery or separate bugs.
Fully agreed.
Let's move this to jQuery. Thanks Michał and all for the fix! |
Concatenating HTML strings in buildFragment is a possible security risk as it creates an opportunity of escaping the concatenated wrapper. It also makes it impossible to support secure HTML wrappers like [trusted types](https://web.dev/trusted-types/). It's safer to create wrapper elements using `document.createElement` & `appendChild`. The previous way was needed in jQuery <4 because IE <10 doesn't accept table parts set via `innerHTML`, even if the element which contents are set is a proper table element, e.g.: ```js tr.innerHTML = "<td></td>"; ``` The whole structure needs to be passed in one HTML string. jQuery 4 drops support for IE <11 so this is no longer an issue; in older version we'd have to duplicate the code paths. IE <10 needed to have `<option>` elements wrapped in `<select multiple="multiple">` but we no longer need that on master which makes the `document.createElement` way shorter as we don't have to call `setAttribute`. jQuery 1.x sometimes needed to have more than one element in the wrapper that would precede parts wrapping HTML input so descending needed to use `lastChild`. Since all wrappers are single-element now, we can use `firstChild` which compresses better as it's used in other places in the code as well. All these improvements, apart from making logic more secure, decrease the gzipped size by 55 bytes. Ref jquerygh-4409 Ref angular/angular.js#17028
Concatenating HTML strings in buildFragment is a possible security risk as it creates an opportunity of escaping the concatenated wrapper. It also makes it impossible to support secure HTML wrappers like [trusted types](https://web.dev/trusted-types/). It's safer to create wrapper elements using `document.createElement` & `appendChild`. The previous way was needed in jQuery <4 because IE <10 doesn't accept table parts set via `innerHTML`, even if the element which contents are set is a proper table element, e.g.: ```js tr.innerHTML = "<td></td>"; ``` The whole structure needs to be passed in one HTML string. jQuery 4 drops support for IE <11 so this is no longer an issue; in older version we'd have to duplicate the code paths. IE <10 needed to have `<option>` elements wrapped in `<select multiple="multiple">` but we no longer need that on master which makes the `document.createElement` way shorter as we don't have to call `setAttribute`. jQuery 1.x sometimes needed to have more than one element in the wrapper that would precede parts wrapping HTML input so descending needed to use `lastChild`. Since all wrappers are single-element now, we can use `firstChild` which compresses better as it's used in other places in the code as well. All these improvements, apart from making logic more secure, decrease the gzipped size by 55 bytes. Ref jquerygh-4409 Ref angular/angular.js#17028
Concatenating HTML strings in buildFragment is a possible security risk as it creates an opportunity of escaping the concatenated wrapper. It also makes it impossible to support secure HTML wrappers like [trusted types](https://web.dev/trusted-types/). It's safer to create wrapper elements using `document.createElement` & `appendChild`. The previous way was needed in jQuery <4 because IE <10 doesn't accept table parts set via `innerHTML`, even if the element which contents are set is a proper table element, e.g.: ```js tr.innerHTML = "<td></td>"; ``` The whole structure needs to be passed in one HTML string. jQuery 4 drops support for IE <11 so this is no longer an issue; in older version we'd have to duplicate the code paths. IE <10 needed to have `<option>` elements wrapped in `<select multiple="multiple">` but we no longer need that on master which makes the `document.createElement` way shorter as we don't have to call `setAttribute`. All these improvements, apart from making logic more secure, decrease the gzipped size by 58 bytes. Closes gh-4724 Ref gh-4409 Ref angular/angular.js#17028 Co-authored-by: Richard Gibson <richard.gibson@gmail.com>
This new version of AngularJS fixes a security issue (CVE-2020-7676). angular/angular.js#17028 https://github.com/angular/angular.js/blob/v1.8.0/CHANGELOG.md Change-Id: Id5a19f74e025c4de4ec2e6170eb4f01a6a606328
…ment Updated To have angular#17028
It appears CVE-2020-7676 was assigned in association with this PR. |
@zelivans The two issues are caused by code in the same area and they're quite similar so it was decided to use one only. Equivalent jQuery issues had two CVEs created. I think it's fine both ways. |
GHSA-mhp6-pxh8-r675 NOTE: since we only support using with jquery 3.5.1 in Salesforce/vlocity we've modified the tests to account for this and drop older versions of jquery.
AngularJS is in LTS mode
We are no longer accepting changes that are not critical bug fixes into this project.
See https://blog.angular.io/stable-angularjs-and-long-term-support-7e077635ee9c for more detail.
Does this PR fix a regression since 1.7.0, a security flaw, or a problem caused by a new browser version?
Yes
What is the current behavior? (You can also link to an open issue here)
<option>
elements in<select>
ones changes parsing behavior, leading to possibly unsanitizing sanitized code. An analogous jQuery advisory: GHSA-jpcq-cgw6-v4j6What is the new behavior (if this is a feature change)?
The issues are fixed. The second one is a breaking change so a new method restoring legacy insecure behavior:
was added.
Does this PR introduce a breaking change?
Yes
Please check if the PR fulfills these requirements
Other information: