Skip to content

Commit

Permalink
Data: Warn about using properties from Object.prototype in data objects
Browse files Browse the repository at this point in the history
  • Loading branch information
mgol committed Jan 14, 2025
1 parent 8bdd4a9 commit d89b44b
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 0 deletions.
86 changes: 86 additions & 0 deletions src/jquery/data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { migratePatchFunc, migrateWarn } from "../main.js";

function patchDataProto( original, options ) {
var i,
apiName = options.apiName,
isInstanceMethod = options.isInstanceMethod,

// `Object.prototype` keys are not enumerable so list the
// official ones here. An alternative would be wrapping
// data objects with a Proxy but that creates additional issues
// like breaking object identity on subsequent calls.
objProtoKeys = [
"__proto__",
"__defineGetter__",
"__defineSetter__",
"__lookupGetter__",
"__lookupSetter__",
"hasOwnProperty",
"isPrototypeOf",
"propertyIsEnumerable",
"toLocaleString",
"toString",
"valueOf"
],

// Use a null prototype at the beginning so that we can define our
// `__proto__` getter & setter. We'll reset the prototype afterwards.
intermediateDataObj = Object.create( null );

for ( i = 0; i < objProtoKeys.length; i++ ) {
( function( key ) {
Object.defineProperty( intermediateDataObj, key, {
get: function() {
migrateWarn( "data-null-proto",
"Accessing properties from " + apiName +
" inherited from Object.prototype is removed" );
return ( key + "__cache" ) in intermediateDataObj ?
intermediateDataObj[ key + "__cache" ] :
Object.prototype[ key ];
},
set: function( value ) {
migrateWarn( "data-null-proto",
"Setting properties from " + apiName +
" inherited from Object.prototype is removed" );
intermediateDataObj[ key + "__cache" ] = value;
}
} );
} )( objProtoKeys[ i ] );
}

Object.setPrototypeOf( intermediateDataObj, Object.prototype );

return function jQueryDataProtoPatched() {
var result = original.apply( this, arguments );

if ( arguments.length !== ( isInstanceMethod ? 0 : 1 ) || result === undefined ) {
return result;
}

// Insert an additional object in the prototype chain between `result`
// and `Object.prototype`; that intermediate object proxies properties
// to `Object.prototype`, warning about their usage first.
Object.setPrototypeOf( result, intermediateDataObj );

return result;
};
}

// Yes, we are patching jQuery.data twice; here & above. This is necessary
// so that each of the two patches can be independently disabled.
migratePatchFunc( jQuery, "data",
patchDataProto( jQuery.data, {
apiName: "jQuery.data()",
isPrivateData: false,
isInstanceMethod: false
} ),
"data-null-proto" );
migratePatchFunc( jQuery.fn, "data",
patchDataProto( jQuery.fn.data, {
apiName: "jQuery.fn.data()",
isPrivateData: true,
isInstanceMethod: true
} ),
"data-null-proto" );

// TODO entry in warnings.md
1 change: 1 addition & 0 deletions src/migrate.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import "./jquery/selector.js";
import "./jquery/ajax.js";
import "./jquery/attributes.js";
import "./jquery/css.js";
import "./jquery/data.js";
import "./jquery/effects.js";
import "./jquery/event.js";
import "./jquery/manipulation.js";
Expand Down
1 change: 1 addition & 0 deletions test/data/testinit.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"unit/jquery/ajax.js",
"unit/jquery/attributes.js",
"unit/jquery/css.js",
"unit/jquery/data.js",
"unit/jquery/deferred.js",
"unit/jquery/effects.js",
"unit/jquery/event.js",
Expand Down
21 changes: 21 additions & 0 deletions test/unit/jquery/data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
QUnit.module( "data" );

QUnit.test( "properties from Object.prototype", function( assert ) {
assert.expect( 6 );

var div = jQuery( "<div>" ).appendTo( "#qunit-fixture" );

div.data( "foo", "bar" );

expectNoMessage( assert, "Regular properties", function() {
assert.strictEqual( div.data( "foo" ), "bar", "data access" );
assert.strictEqual( jQuery.data( div[ 0 ], "foo" ), "bar", "data access (static method)" );
} );

expectMessage( assert, "Properties from Object.prototype", 2, function() {
assert.ok( div.data().hasOwnProperty( "foo" ),
"hasOwnProperty works" );
assert.ok( jQuery.data( div[ 0 ] ).hasOwnProperty( "foo" ),
"hasOwnProperty works (static method)" );
} );
} );
9 changes: 9 additions & 0 deletions warnings.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,15 @@ This is _not_ a warning, but a console log message the plugin shows when it firs

**Solution:** Always pass string values to `.css()`, and explicitly add units where required. For example, use `$.css("line-height", "2")` to specify 200% of the current line height or `$.css("line-height", "2px")` to specify pixels. When the numeric value is in a variable, ensure the value is converted to string, e.g. `$.css("line-height", String(height))` and `$.css("line-height", height+"px")`.

### \[data-null-proto\] Accessing properties from jQuery.data() inherited from Object.prototype is removed
### \[data-null-proto\] Setting properties from jQuery.data() inherited from Object.prototype is removed
### \[data-null-proto\] Accessing properties from jQuery.fn.data() inherited from Object.prototype is removed
### \[data-null-proto\] Setting properties from jQuery.fn.data() inherited from Object.prototype is removed

**Cause:** As of jQuery 4.0.0, data objects no longer inherit from `Object.prototype`. This includes properties like `__proto__` or `hasOwnProperty`.

**Solution:** Don't use properties inherited from `Object.prototype` on data objects. Instead of `jQuery.data( node ).hasOwnProperty( "foo" )` use `Object.hasOwn( jQuery.data( node ), "foo" )` or, if you need to support older browsers like IE 11, use `Object.prototype.hasOwnProperty.call( jQuery.data( node ), "foo" )`.

### \[self-closed-tags\] JQMIGRATE: HTML tags must be properly nested and closed: _(HTML string)_

**Cause:** jQuery 3.5.0 changed the way it processes HTML strings. Previously, jQuery would attempt to fix self-closed tags like `<i class="test" />` that the HTML5 specification says are not self-closed, turning it into `<i class="test"></i>`. This processing can create a [security problem](https://nvd.nist.gov/vuln/detail/CVE-2020-11022) with malicious strings, so the functionality had to be removed.
Expand Down

0 comments on commit d89b44b

Please sign in to comment.