-
Notifications
You must be signed in to change notification settings - Fork 23k
Add a page for JavaScript prototype pollution attack #41260
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Thanks! I actually thought about writing this not long ago. However, this seems to be covered by XSS or supply chain attack already because you need to have a vector in order to inject code for prototype pollution, and if you get someone to execute your code anyway, you can do many other bad things as well. |
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't think through this completely, but there are at least two things that are missing:
- Not all prototype pollution involves adding properties to
Object.prototype. Sometimes, they overwrite primordial methods and "hook into" language mechanisms to execute arbitrary code. An infamous case is overridingPromise.prototype.then, which would be called for everyawaitoperation. See for example: nodejs/node#59699 and https://github.com/tc39/proposal-thenable-curtailment. This is also whySymbol.speciesis considered a mistake: https://github.com/tc39/proposal-rm-builtin-subclassing. - "Freezing the prototype" is good, but instead of recommending Immer, we could recommend something actually built for this purpose (freezing every built-in object). That is SES.
|
So your description is not far from how prototype pollution works. Generally, you either get to execute code directly (for example, in an iframe or via |
Co-authored-by: Joshua Chen <sidachen2003@gmail.com>
|
Thanks @Josh-Cena! I've tried to work in your feedback in 2ce35f2 and I've accepted your suggestions. Let me know what you think about that. Also, should we add links to this from some JS docs? I guess at a minimum from |
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a bug to fix, and then I'm looking at https://github.com/tc39/proposal-symbol-proto, which has a lot of useful points:
- No code injection (data-only): I already added a bit about it, but perhaps you want to make it even clearer that CSP won't help.
- You can also reach
Object.prototypeviaresult.constructor.prototype.test, which is longer, but doesn't use__proto__.
And yes we should link to this page from __proto__, Object, and the prototype chain guides.
| result.test; // "polluted" | ||
| {}.test; // "polluted" | ||
| "".test; // "polluted" | ||
| [].test; // "polluted" | ||
| // ... |
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ummm... This is wrong. In this case you constructed a single object, result, whose prototype is { test: "polluted" }. You did not successfully taint every object (although this may still be an issue). In order to do that, you need to have code that assigns properties, where the object being assigned could turn out to be Object.prototype:
result[x][y] = z;In this case, if result is a plain object, then you can set x = "__proto__" and y = "test" to successfully inject it.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I removed this part for now. I struggle to illustrate it properly.
|
|
||
| ### Node.js flag `--disable-proto` | ||
|
|
||
| If you are in a Node.js environment, you can disable `Object.prototype.__proto__` with the `--disable-proto=MODE` option where `MODE` is either `delete` (the property is removed entirely), or `throw` (accesses to the property throws an exception with the code `ERR_PROTO_ACCESS`). |
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A simple trick for non-Node is delete Object.prototype.__proto__
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this a trick or advice we should give?
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know any pitfalls of this, so this is an advice. Perhaps the only pitfall is, like freezing internals, you may have code that runs before the realm lockdown (polyfills etc) and those codes are still insecure.
| ### Create a JavaScript object without a prototype | ||
|
|
||
| The {{jsxref("Object.create()", "Object.create(null)")}} function allows you to create a new empty object without a prototype. Because of this, there won't be any prototype pollution issues: | ||
|
|
||
| ```js | ||
| let obj = Object.create(null); | ||
| obj.__proto__; // undefined | ||
| obj.constructor; // undefined | ||
| obj["__proto__"]["a"] = 1; // TypeError: Cannot set properties of undefined | ||
| ``` |
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Create "what" object without a prototype? All objects?
I think it is worth spelling out more like "If you are populating objects from user supplied data then they should be ....". This is true for the other sections too.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I tried to make this more clear by saying that it is not the default and that would be needed for every object creation.
| - Create empty objects with `Object.create(null)`. | ||
| - Use `Map` and `Set` objects instead. | ||
| - Consider freezing your own objects, for example with a library like [Immer](https://immerjs.github.io/immer/). | ||
| - Consider freezing built-in objects, for example by using the [SES](https://github.com/endojs/endo/tree/master/packages/ses#ses) shim. | ||
| - Validate your object structure with a schema. | ||
| - Use `--disable-proto` in Node.js. |
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above "what object". I assume disabling proto etc only disables proto on your code? How might it affect imported libraries etc?
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addded "to disable Object.prototype.__proto__." because as I understand it, it just disables the special __proto__ property.
|
|
||
| Not all prototype pollution involves adding properties to `Object.prototype`, though. Other prototypes can also be polluted, like when overriding {{jsxref("Promise.prototype.then")}} which would be called for every `await` operation, and more. | ||
|
|
||
| To see the effect of prototype pollution, we can look at the how the following {{domxref("fetch()")}} call can be changed completely. By default, it is a {{HTTPMethod("GET")}} request, but because we polluted the `Object` object's prototype with two new default properties, the `fetch()` call is now transformed into a {{HTTPMethod("POST")}} request. |
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So this is obviously not the intent, but I kind of feel that this is a bit abstract - why might this be a problem? Would be great to be a little more concrete on what you might now do having made this change.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added another example that Aaron suggested on srcdoc to make things more spooky.
| fetch("https://example.com", { | ||
| mode: "cors", | ||
| }); | ||
| // Promise {status: "pending", body: "a=1", method: "POST"} |
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While I think that this is as simple and very clear example, I wonder whether we can do more here (or somewhere else in the doc) to really drive home the point of "Prototype Pollution is scary and a big security issue".
In addition to overriding GET / POST, some other attacks that could be a lot more scary-seeming:
srcdoconObject.prototype-> XSS- default configurations on sanitizers
- some existing CVEs and/or known faults in particularly famous NPM packages that allow for exploitability in the wild
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I added srcdoc and sanitizer config as other examples (to at least mention the fact that some objects might be more sensitive than others and deserve extra attention). Thank you for these pointers, they made it even scarier for me :)
|
|
||
| There are a few options to mitigating prototype pollution vulnerabilities. This section presents them. | ||
|
|
||
| ### Create a JavaScript object without a prototype |
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While this is a very very simple way to stop PP on the most sensitive objects, the onus is still on the instantiators of every object to run this and therefore is not secure-by-default. Is this nuance worth mentioning here?
Also, if that is the case... should the Object.freeze solution be promoted higher on the list of solutions?
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I tried to make it more clear that the default behavior is not a null-prototype unfortunately.
I moved the Map and Set mitigation below Object.freeze now, so it is second after Object.create(null). I hope that makes more sense.
|
|
||
| Instead of using JavaScript objects, use {{jsxref("Map")}} or {{jsxref("Set")}} objects. These are more modern data structures not vulnerable to prototype pollution issues. | ||
|
|
||
| ### Freezing the prototype |
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it worth mentioning the slightly related mitigations Object.seal and Object.preventExtensions ?
https://portswigger.net/web-security/prototype-pollution/preventing
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Object.seal and Object.preventExtensions are both mentioned in this section now. Object.freeze is presented as the tightest option
Co-authored-by: Hamish Willee <hamishwillee@gmail.com> Co-authored-by: Joshua Chen <sidachen2003@gmail.com>
wbamberg
left a comment
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't really a complete review, but overall, it's not very clear to me what specific things a developer might be doing that makes them vulnerable to this threat, and which defenses they need to implement against which threats, based on what they are doing.
| To understand how JavaScript prototypes work, read the following MDN articles: | ||
|
|
||
| - [Inheritance and the prototype chain](/en-US/docs/Web/JavaScript/Guide/Inheritance_and_the_prototype_chain) | ||
| - [Working with objects](/en-US/docs/Web/JavaScript/Guide/Working_with_objects) |
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's very helpful to start out by sending someone to two very long and fiercely technical articles, as a preparation for understanding this article. For one thing, there's a lot in those articles, and they're not all about prototypes. For another, our job here is to give someone who might be quite inexperienced practical guidance about how to protect themselves against concrete threats. I think if we say effectively "to understand this threat, you have to read all that", they will switch off.
It's fine to include these links and invite people to read them, but we should do our best to explain the setup in as accessible a way as possible here, and then present these links as "if you want to learn more about this...".
BTW I tried to explain this accessibly in https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Advanced_JavaScript_objects/Object_prototypes.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks Will, your suggested text is a lot more accessible and I will merge that in. We should probably also link to the Learn article
|
|
||
| In a nutshell, every object in JavaScript has a `prototype` property which is an object itself and that prototype has its own prototype, too. This is called the JavaScript prototype chain. The chain ends when we reach an object that has `null` for its own prototype. The power of the prototype chain is that it allows an object to inherit properties and methods from its parent. However, a bad practice that JavaScript allows, is that the prototypes of objects can be changed during runtime which makes it possible to install properties at unintended places with malicious values. This even includes the built-in default objects, like the {{jsxref("Object")}} object, which is likely in the prototype chain of the majority of objects in your codebase and therefore makes this attack especially dangerous. | ||
|
|
||
| A key attack vector is the special [`Object.prototype.__proto__`](/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/proto) property, and how adding a property to `Object.prototype` will make that property accessible on ("polluted to") every object in your application. The `__proto__` property is often used, but you can also reach `Object.prototype` via `yourObject.constructor.prototype`. |
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We say __proto__ is special, but don't say how it is special, or even what it is, and the second part of that sentence doesn't seem to follow from the first.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I removed "special" and I changed this to have two paragraphs with the goal to explain two things:
- The _
_proto__properties are used very often for this attack but, as Josh says, the constructor.prototype property can also be used. - Object.prototype is most commonly the target of this. But other prototypes (like Promise.prototype) could also be targets.
| ``` | ||
|
|
||
| Any configuration objects, like `fetch()`'s {{domxref("RequestInit")}} object in the code example above, or the instantiation of `<iframes>`, or configuration of sanitizers ({{domxref("SanitizerConfig")}} objects) are some of the most sensitive objects and are often targets of prototype pollution attacks. | ||
|
|
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I struggle with these because they're not attacks. Yes, if you can run JS in my context (to do things like Object.prototype.method = "POST";) you can do this, but if you can run JS in my context it's game over already, isn't it?
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think these example are more useful to show how prototypes are malleable in JavaScript (as an illustration of the previous discussion of prototypes being malleable) than they are as examples of attacks.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree and I struggle to explain the first step here. I understand how it is game over if you are already able to pollute prototypes, but I don't understand how it usually comes to that. I mean anyone could do Object.prototype.method = "POST"; in devtools even. Would that make things horrible already? Do pentesters test this way?
I actually try to mention this in the "Vulnerable sources" section. So there I mention URL query strings which https://github.com/BlackFan/client-side-prototype-pollution seems to be all about. And I mention JSON.parse which apparently also can go wrong if you deal with JSON input coming from untrusted sources.
|
|
||
| ### Vulnerable sources | ||
|
|
||
| In order to pollute objects, the attacker needs a way to add arbitrary properties to prototype objects. This may happen as a consequence of [XSS](/en-US/docs/Web/Security/Attacks/XSS), in which the attacker gains direct access to the page's JavaScript execution environment. However, this can also happen without code injection, such as by constructing a payload that contains the `__proto__` property key will later get merged into another object via [spreading](/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax), [`for...in` loops](/en-US/docs/Web/JavaScript/Reference/Statements/for...in), etc., turning it into an assignment operation and therefore triggering the setter. |
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bit vague. As a developer, I want to know: exactly what things might I do that would make me vulnerable? For example, with XSS it's pretty clear: including potentially-user-supplied content in pages without sanitizing it. What's the equivalent?
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See #41260 (comment). I don't know the answer for sure but my guess is that this comes via URL query strings, untrusted JSON input, and maybe even just devtools.
|
|
||
| ## Defenses against prototype pollution | ||
|
|
||
| There are a few options to mitigating prototype pollution vulnerabilities. This section presents them. |
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a developer, I want to know:
- when do I need to implement defenses
- in these cases, which defenses do I NEED, and which are alternates for each other
- which are nice-to-have, defenses in depth
What's the best practice recommendation? I don't think it is so helpful to say "here's a long list of things you can do".
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know the answer. I can try to guess. Would be good to hear from an SME.
For objects you create and fully control:
- Evaluate if an object is needed or if a Map or Set would be the better choice
- If you're forced to create an object, use Object.create(null), especially for sensitive objects like FetchInit or SanitizerConfig
- If you can't create an object with Object.create(null), consider freezing your object, or make it immutable using a library like Immer
- If you need to create complex and unfrozen objects with a prototype, consider validating your object structure with a schema and reject unneeded properties by setting
additionalPropertiestofalse.
For built-in and third party objects:
- Consider freezing built-in and third party objects, for example by using the SES shim.
Runtime defenses:
- Use
--disable-protoin Node.js to disableObject.prototype.__proto__. - Use
delete Object.prototype.__proto__in non-Node environments.
|
|
||
| ### Use `Map` and `Set` instead | ||
|
|
||
| Instead of using JavaScript objects, use {{jsxref("Map")}} or {{jsxref("Set")}} objects. These are more modern data structures not vulnerable to prototype pollution issues. See the `Map` documentation for a [comparison between Maps and Objects](/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#objects_vs._maps). The {{jsxref("Map.prototype.get()")}} method will always only return the property that have been set directly on the `Map`. |
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Instead of using JavaScript objects, use {{jsxref("Map")}} or {{jsxref("Set")}} objects." but of course we use JS objects all the time so we can't really mean that can we? I'm not clear how this would protect against (say) the fetch() pollution attack above, does it? So what's the specific vulnerability we're mitigating here?
Co-authored-by: wbamberg <will@bootbonnet.ca>
…raph
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think if you want to have an overarching principle for defense against PP, it would be this: (1) don't make prototype modifications happen, (2) even if modifications happen, don't access properties on Object.prototype and you should be safe.
(1) can be achieved via freezing prototypes, --disable-proto, delete Object.prototype.__proto__ etc. Another thing you can mention is to avoid dynamic property modification (of the form obj[k] = v) unless you know deterministically what k can be (a la JSON validation). If you rule out __proto__, constructor, prototype as keys then you are generally safe.
(2) looks like this (fetch falls into this category):
function doSomething(options = {}) {
if (options.isAdmin) // ...
}In this case, if options.isAdmin is undefined, it may walk up the prototype chain. The correct defense here, besides of course requiring your user to pass null-prototype objects or a Map (the latter being an API change), is to add an Object.hasOwn(options, "isAdmin") check to avoid lookup on the prototype, or explicitly set a default like options = { isAdmin: false }.
It could also look like this:
for (const key in payload) {
doSomething(payload[key]);
}for...in traverses the protoype. The correct way is to only visit own keys, i.e., for (const key of Object.keys(payload)) {.
In other words, audit all your object property accesses and make sure you know deterministically that the property exists on the object itself. When you are getting keys on objects that may not possess the key, add an Object.hasOwn check; when you are traversing keys on objects, use for...of + Object.keys instead of for...in.
Also perhaps we are making it too much like PP is a subset of XSS, but the danger of PP is exactly that it's unrelated to XSS—it's a data-only attack that only requires JSON communication.
| // toString() is defined on the prototype of `Array.prototype`, | ||
| // which is `Object.prototype` |
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also false: this method is defined on Array.prototype which shadows the one from Object.prototype (otherwise you should see [object Array]). If you want an example that calls Object.prototype methods, you need to do myArray.propertyIsEnumerable("length").
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK maybe my example is not good, but having an example here is helpful I think!
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
--> 801ab9d
|
So in 63c8954 I committed my (opinionated) thoughts from #41260 (comment) to the Defense summary checklist and also worked it into each of the sections. I also added a new section "Avoid lookups on the prototype" which is inspired by Josh's last comment. |
|
Are you fine if I push directly to your branch, or would you prefer a PR to your fork? I have some reordering/rewriting suggestions which are too complicated to propose as comments. |
|
I'm fine with direct pushes to this PR (granular commits appreciated so I can review what was changed). Thank you Josh! |
|
I pushed two commits. My first one is a rewrite; I hope it makes sense what I added and removed, but LMK if anything's fishy. My second one is a reorder; I reordered both the anatomy and the defenses into two lines, "pollution (and avoiding)" and "exploitation (and avoiding)". |
| ```js | ||
| const config = deepFreeze({ | ||
| url: "https://api.example.org", | ||
| headers: { | ||
| Authorization: "some_token", | ||
| }, | ||
| }); | ||
|
|
||
| config.headers.Authorization = "hacked_token"; // fails thanks to deepFreeze | ||
| Object.freeze(Object.prototype); | ||
| const obj = {}; | ||
| const key1 = "__proto__"; | ||
| const key2 = "a"; | ||
| obj[key1][key2] = 1; // fails silently in non-strict mode | ||
| obj.a; // undefined | ||
| ``` | ||
|
|
||
| The `deepFreeze` approach is not handling circular references by default, so to create true immutable objects, you may want to look into using a library like [Immer](https://immerjs.github.io/immer/). You could also consider the [SES](https://github.com/endojs/endo/tree/master/packages/ses#ses) shim for [Hardened JavaScript](https://hardenedjs.org) to freeze every built-in object. |
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note, I removed this paragraph about deep-freezing your own objects. Freezing objects you created don't make sense outside the context of XSS, because (1) if your object is expected to change dynamically, then you can't freeze it anyway (2) if your object is not expected to change and you don't have any code of the kind obj[key] = value, then you aren't susceptible to any data-only modification attempts. It's only built-in objects (i.e. prototypes) that are vulnerable and need to be frozen, so SES is good enough.
| Another dangerous pollution attack target is the {{domxref("HTMLIframeElement.srcdoc")}} property which specifies the content of an {{HTMLElement("iframe")}} element. By overriding its value, it could potentially be possible to execute arbitrary code. | ||
|
|
||
| ```js | ||
| Object.prototype.srcdoc = "<script>alert(1)<\/script>"; |
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 this is real. In HTML, the source of truth is the DON srcdoc attribute, not its JavaScript reference's srcdoc IDL property. It just happens that the IDL setter is a shorthand for setting the HTML attribute, but I don't think this attack will do anything...? Or I am missing context about how the iframe is created?
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@aaronshim can you elaborate? In #41260 (comment) you said:
In addition to overriding GET / POST, some other attacks that could be a lot more scary-seeming:
srcdoconObject.prototype-> XSS
How does attacking srcdoc usually work in the context of prototype pollution?
|
Thank you @Josh-Cena, I really appreciate your changes. I think the article flows quite nicely now. |
|
Iterating here on the comments about how prototype pollution usually occurs in the first place and why it is scary. I think it makes sense to differentiate between client-side and server-side prototype pollution, as their sources and impacts vary. The most common source of client-side prototype pollution is URL parsing, as it is something that can be controlled by an attacker. Either via the query string or fragment. Another potential, though less common source can be a // Attacker indirectly causes the following pollution
Object.prototype.body = "a=1";
Object.prototype.method = "POST";
fetch("https://example.com", {
mode: "cors",
});
// Promise {status: "pending", body: "a=1", method: "POST"}But let's assume your banking website is vulnerable and you can do something like this: Object.prototype.body = "action=transfer&amount=1337&to=1337-1337-1337-1337";
Object.prototype.method = "POST";
fetch("https://example.bank", {
mode: "cors",
});
// Promise {status: "pending", body: "action=transfer&amount=1337&to=1337-1337-1337-1337", method: "POST"}That's pretty bad, right? Server-side prototype pollution often occurs when parsing JSON data received from a client and then "deep merging" objects. But it may also occur later when retrieving data from a database and constructing a new object, for example. There are many more possibilities here. |
|
Thank you, @moz-mdauer! I updated the abstract fetch code example with your specific bank example, as it is more bad and maybe brings across the spookiness of this even more. Also thanks for the pointer to server-side prototype pollution! I think it is a very valuable resource, so I added it along with the client-side prototype pollution repo to the "See also" of this article. Overall, I think the article explains this more in the context of the client-side but that's quite typical for the MDN audience and I think that's alright. I've heard good feedback on this article from various people now, so I'm planning to merge it later today or tomorrow. Thanks again, everyone, for your contributions and your feedback! |
wbamberg
left a comment
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you Florian, Josh, and everyone else who's contributed to this!
Description
This PR adds a new page to our list of "attacks", this one is on JavaScript Prototype Pollution.
Generally, I'm not sure if I covered everything that needs to appear here. I think I'm somewhat happy about the defenses part, but explaining how to actually get into to polluting objects might need some more words?
Also, I think this needs integration into JS docs (at a minimum some linkage).
Motivation
Developing a series of guides about common attacks on websites, explaining for each attack: how it works, the impact, and how to defend against it.
Additional details
Would love some feedback from @wbamberg, @aaronshim and maybe @Josh-Cena.
Related issues and pull requests
None, I think.