Prevent merging __proto__ property#48
Conversation
|
I think calling this an "attack" is a significant overexaggeration. This library is meant to be an exact copy of jQuery's algorithm, warts and all. I'm not solely convinced by this article that this is a real problem in practice - one worth deviating from jQuery's original algorithm. Separately, if this isn't going to get the actual [[Prototype]], I'd expect it to end up matching the JSON object - in other words, having an own |
|
Thanks so much for looking at this! I'm definitely with you on giving the target an own I do still think it has the potential to be a problem in practice. A well-behaved app ought to sanitize user-provided JSON before sending it through |
|
|
||
| // If name is '__proto__', define it as an own property on target | ||
| var setProperty = function setProperty(target, options) { | ||
| if (options.name === '__proto__') { |
There was a problem hiding this comment.
what happens if Object.defineProperty is unavailable, like in ES3 engines?
There was a problem hiding this comment.
I've been thinking about that a bit. It might be good to check for the existence of Object.defineProperty and fall back to regular assignment.
This means it would still be possible to modify prototypes in pre-ES5 environments. That seems fine in practice. Some browsers will fall back, but Node won't have to. iojs and Node 0.8 have Object.defineProperty available.
(To be frank, Node 0.8 has bigger security concerns anyway)
| // Return a new object instead of __proto__ if '__proto__' is not an own property | ||
| var getProperty = function getProperty(obj, name) { | ||
| if (name === '__proto__' && !hasOwn.call(obj, name)) { | ||
| return {}; |
There was a problem hiding this comment.
should this not be undefined in that case?
| t.notOk(target.george); | ||
| t.ok(Object.prototype.hasOwnProperty.call(target, '__proto__')); | ||
| var name = '__proto__'; | ||
| t.equal(target[name].george, 1); |
There was a problem hiding this comment.
t.deepEqual(target.__proto__, { george: 1 }) would also work
There was a problem hiding this comment.
For sure!
The reason for the var name = '__proto__' gymnastics: the linter discourages both target.__proto__ (no-proto) and target['__proto__'] (dot-notation). After some thought, I want to drop the gymnastics and use a comment to disable no-proto on that line.
|
I'm digging into the CI checks a little bit. So far, I've learned:
|
|
@mnespor Regarding eslint, and covert, and the general testing landscape: I'll update travis.yml in master and rebase this PR again in a few hours. |
ebd56b3 to
3f92a15
Compare
|
Rebased; tests are now failing in 0.8 and 0.10, and they seem to be failing properly. Note that when you're testing locally, only |
| t.ok(Object.prototype.hasOwnProperty.call(target, '__proto__')); | ||
| t.deepEqual(target.__proto__, { george: 1 }); // eslint-disable-line no-proto | ||
| // this test isn't valid for earlier versions of V8, which strip __proto__ during JSON.parse() | ||
| if (Object.prototype.hasOwnProperty.call(malicious, '__proto__')) { |
There was a problem hiding this comment.
Perhaps instead of this, we could do var malicious = { fred: 1 }; if (Object.defineProperty) { Object.defineProperty(malicious, '__proto__', { value: { george: 1 } }; }?
JSON.parse is just one way a malicious/surprising object could appear, but we should try to test this in earlier v8s too.
There was a problem hiding this comment.
I like that a lot. It's doing something I didn't expect in 0.8, however:
> var malicious = { fred: 1 }
undefined
> Object.defineProperty(malicious, '__proto__', { value: { george: 1 }, enumerable: true })
{ fred: 1, [__proto__]: { george: 1 } }
> malicious.__proto__
{}
> malicious.hasOwnProperty('__proto__')
true
> Object.keys(malicious)
[ 'fred', '__proto__' ]
> malicious[Object.keys(malicious)[0]]
1
> malicious[Object.keys(malicious)[1]]
{}
(in newer Nodes, it does this instead):
> Object.defineProperty(malicious, '__proto__', { value: { george: 1 }, enumerable: true })
{ fred: 1, __proto__: { george: 1 } }
> malicious.__proto__
{ george: 1 }
There was a problem hiding this comment.
interesting - it seems in node 0.8, Object.getOwnPropertyDescriptor(malicious, '__proto__').value gives the expected output. This seems to be a bug in these node versions; but it'd be fine to use this approach in the tests.
There was a problem hiding this comment.
That's very interesting. It might not be fixable from the test side alone. The library code would also have to do copy = Object.getOwnPropertyDescriptor(options, name).value instead of copy = options[name] to get the real value of an own __proto__ on Node 0.8.
Pure druthers, I'd change the test to use Object.defineProperty() instead of JSON.parse(), but continue to skip this test case on 0.8 and 0.10.
Alternatively, this could go into the library and the test would pass on 0.8:
var getProperty = function getProperty(obj, name) {
if (name === '__proto__') {
if (!hasOwn.call(obj, name)) {
return undefined;
} else if (Object.getOwnPropertyDescriptor) {
return Object.getOwnPropertyDescriptor(obj, name).value;
}
}
return obj[name];
};
The performance characteristics of Object.getOwnPropertyDescriptor() aren't ideal, but I trust that branch wouldn't run often in production.
What do you think?
There was a problem hiding this comment.
I don't think performance is particularly important here anyways :-) let's go for it.
|
@ljharb That change ought to get the test suite passing on Node 0.8 and 0.10. If you get some time free, could you give it a review, please? |
|
Hi, just wanted to check in. Is this ready to merge? |
1a5c464 to
0e68e71
Compare
|
v3.0.2 and v2.0.2 have been released with this change. |
|
Thank you kindly! |
Hi there! Would you be open to preventing deep cloning of properties named
__proto__to avoid the attack described in JavaScript Prototype Poisoning Vulnerabilities in the Wild?