Skip to content

Commit 872affd

Browse files
committed
Manipulation: Avoid concatenating strings in buildFragment
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
1 parent 40c3abd commit 872affd

File tree

3 files changed

+25
-13
lines changed

3 files changed

+25
-13
lines changed

src/manipulation/buildFragment.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import jQuery from "../core.js";
22
import toType from "../core/toType.js";
33
import isAttached from "../core/isAttached.js";
4+
import arr from "../var/arr.js";
5+
import document from "../var/document.js";
46
import rtagName from "./var/rtagName.js";
57
import rscriptType from "./var/rscriptType.js";
68
import wrapMap from "./wrapMap.js";
@@ -35,15 +37,17 @@ function buildFragment( elems, context, scripts, selection, ignored ) {
3537

3638
// Deserialize a standard representation
3739
tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase();
38-
wrap = wrapMap[ tag ] || wrapMap._default;
39-
tmp.innerHTML = wrap[ 1 ] + jQuery.htmlPrefilter( elem ) + wrap[ 2 ];
40+
wrap = wrapMap[ tag ] || arr;
4041

41-
// Descend through wrappers to the right content
42-
j = wrap[ 0 ];
43-
while ( j-- ) {
44-
tmp = tmp.lastChild;
42+
// Create wrappers & descend into them.
43+
j = wrap.length;
44+
while ( --j > -1 ) {
45+
tmp.appendChild( document.createElement( wrap[ j ] ) );
46+
tmp = tmp.firstChild;
4547
}
4648

49+
tmp.innerHTML = jQuery.htmlPrefilter( elem );
50+
4751
jQuery.merge( nodes, tmp.childNodes );
4852

4953
// Remember the top-level container

src/manipulation/wrapMap.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
1-
// We have to close these tags to support XHTML (#13200)
21
var wrapMap = {
32

43
// Table parts need to be wrapped with `<table>` or they're
54
// stripped to their contents when put in a div.
65
// XHTML parsers do not magically insert elements in the
76
// same way that tag soup parsers do, so we cannot shorten
87
// this by omitting <tbody> or other required elements.
9-
thead: [ 1, "<table>", "</table>" ],
10-
col: [ 2, "<table><colgroup>", "</colgroup></table>" ],
11-
tr: [ 2, "<table><tbody>", "</tbody></table>" ],
12-
td: [ 3, "<table><tbody><tr>", "</tr></tbody></table>" ],
13-
14-
_default: [ 0, "", "" ]
8+
thead: [ "table" ],
9+
col: [ "colgroup", "table" ],
10+
tr: [ "tbody", "table" ],
11+
td: [ "tr", "tbody", "table" ]
1512
};
1613

1714
wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;

test/unit/manipulation.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2969,3 +2969,14 @@ QUnit.test( "Sanitized HTML doesn't get unsanitized", function( assert ) {
29692969
test( "<noembed><noembed/><img src=url404 onerror=xss(12)>" );
29702970
}
29712971
} );
2972+
2973+
QUnit.test( "Works with invalid attempts to close the table wrapper", function( assert ) {
2974+
assert.expect( 3 );
2975+
2976+
// This test case attempts to close the tags which wrap input
2977+
// based on matching done in wrapMap which should be ignored.
2978+
var elem = jQuery( "<td></td></tr></tbody></table><td></td>" );
2979+
assert.strictEqual( elem.length, 2, "Two elements created" );
2980+
assert.strictEqual( elem[ 0 ].nodeName.toLowerCase(), "td", "First element is td" );
2981+
assert.strictEqual( elem[ 1 ].nodeName.toLowerCase(), "td", "Second element is td" );
2982+
} );

0 commit comments

Comments
 (0)