v5.5.1 - Security Hardening: ACL Scoping, Cache, and Pipeline Guards
Security Hardening Patch
A focused security hardening release. Mongo-direct queries inside Parse.with_session now respect the ambient session token instead of falling through to master-key reads, the Redis HTTP and embedding caches drop Marshal in favor of JSON serialization, credential columns are recursively stripped from joined sub-documents, and several aggregation-terminal and credential-oracle paths are tightened.
Changes
Mongo-direct reads inside Parse.with_session are now scoped, not master
- FIXED: A query that auto-routes to the mongo-direct path because of a direct-only constraint (for example a geo
$near/$geoIntersectsquery) now honors the ambient session token set byParse.with_session(token). Previously the mongo-direct auth resolver consulted only the query's ownsession_token=/scope_to_user/scope_to_roleand ignored the fiber-local ambient session, so in server mode it fell through to a master-key read with no ACL/CLP enforcement — returning rows the session was not permitted to see, even though every REST query in the samewith_sessionblock was correctly scoped. The resolver now mirrorsParse::Client#requestprecedence: an explicit per-query token wins, then the ambient session, then the master-key fallback; an explicituse_master_key: trueis a deliberate admin call and still skips the ambient. Routing also accepts the ambient on non-master clients (Parse.client_modeor a user-scoped client), so such a query runs scoped rather than raising.
Boolean property coercion no longer treats the string "false" as true
- FIXED: A
:booleanproperty assigned a string now coerces via ActiveModel's boolean caster instead of raw Ruby truthiness. Previously the coercion wasval ? true : false, so the strings"false","0", and"off"— exactly what arrives on a Rails-form or query-string ingestion path — all coerced totrue, silently flipping a boolean the wrong way (for example anarchivedflag or an application-defined access gate). String forms now map correctly ("false"/"0"/"off"tofalse), a blank string is treated as unset (nil), and native booleans from Parse wire JSON pass through unchanged.
Deprecation warning for setting ACL via mass-assignment
- DEPRECATED: Setting
acl/ACLthrough mass-assignment (Parse::Object#attributes=) now emits a one-time security warning. Mass-assigning an ACL from a caller-supplied hash — for example a controller doingrecord.attributes = paramswithout StrongParameters — lets an attacker grant unintended access by sending anACLkey ({"ACL" => {"*" => {"write" => true}}}). The behavior is unchanged this release (the ACL is still applied), but the supported path is the explicitrecord.acl = ...setter, and a future release may block ACL mass-assignment. The constructor formKlass.new(acl: ...)is unaffected and does not warn.
Redis cache values serialized as JSON instead of Marshal
- FIXED:
Parse::Cache::Redisnow serializes cached HTTP responses as JSON rather than Marshal. The Moneta-Redis store Marshals values by default, so every cache hit ranMarshal.loadon the bytes returned by Redis. Against a shared, unauthenticated, or plaintext-redis://cache, an attacker able to write the cache could plant a crafted Marshal payload that executed code on deserialization. The wrapper now disables Moneta's value serializer (value_serializer: nil) and JSON-encodes/decodes values itself; an undecodable value (including any legacy Marshal entry) is treated as a cache miss rather than deserialized. Cache keys are unchanged. No application code changes are required; existing cached entries are transparently refetched and re-stored in the new format on first access. - FIXED: The
cache: "redis://..."shorthand onParse::Client.new/Parse.setupnow builds aParse::Cache::Redisstore instead of a bareMoneta.new(:Redis, ...), so it gets the same JSON value serialization and is not subject to the Marshal deserialization issue above. - CHANGED: The caching middleware stores response entries with string keys so they round-trip losslessly through the JSON serialization. Reads accept both string and legacy symbol keys.
- FIXED:
Parse::Embeddings::Cache::MonetaStorenow JSON-encodes cached embedding vectors instead of relying on the Moneta store's default Marshal value serializer, closing the sameMarshal.load-on-read deserialization vector for the embedding cache (whose key is derived from often-user-supplied text). It also emits a one-time warning when handed a Marshal-serializing store and recommendsvalue_serializer: nil. - CHANGED: Documentation for Redis-backed caches, the embedding cache, and the synchronize-create lock store (
Parse.synchronize_create_store) now builds the Redis store viaParse::Cache::Redisorvalue_serializer: nilso a rawMoneta.new(:Redis, ...)no longer leaves Marshal on the read path.
Internal columns stripped from joined documents on mongo-direct reads
- FIXED:
Parse::MongoDB.aggregatenow recursively strips Parse-internal credential columns (_hashed_password,_session_token,_auth_data_*,_rperm/_wperm, ...) from every result row and every embedded sub-document for scoped (non-master) callers. Previously a scoped caller could embed a foreign class (e.g._Useror_Session) into an arbitrary alias via$lookup/$graphLookup/$unionWithand read back password hashes, OAuth tokens, and session tokens: the per-classprotectedFieldsstrip is keyed on the outer class, and the ACL sub-document walk only drops ACL-failing sub-documents, so neither covered the aliased foreign document. A newParse::PipelineSecurity.redact_internal_fields_deep!runs as the final redaction step. Structural columns (_id,_p_*,_acl, timestamps) are preserved, so object and ACL reconstruction are unaffected; master-key reads are unchanged.
Hardened mongo-direct aggregation terminals
- FIXED: Credential columns (
_hashed_password,_session_token,_auth_data_*,_email_verify_token,_perishable_token, ...) used as a$matchfield name are now refused unconditionally on the mongo-direct path — even on a pipeline running withallow_internal_fields: true(the flag that lets SDK-emitted_rperm/_wpermreferences through forreadable_by_role/publicly_readable). Previously the*_directterminals (count_direct,results_direct,distinct_direct, the direct group-by helpers) passedallow_internal_fields: trueunconditionally, so a query whosewherereferenced a credential column compiled into a$matchkey that bypassed the internal-field screen — a count/match oracle that could bisect a bcrypt hash or session token. The ACL columns (_rperm/_wperm/_tombstone) remain gated byallow_internal_fields, soreadable_by_rolestill works. - FIXED:
Parse::Query#aggregateand#aggregate_from_querynow treat a scoped query (session_token/scope_to_user/scope_to_role) as authoritative over an explicitmongo_direct: false. Previously passingmongo_direct: falseon a scoped aggregation skipped the fail-closed guard and routed to Parse Server's master-key-only REST/aggregateendpoint, running the aggregation unscoped (no ACL, CLP, orprotectedFields). A scoped aggregation now promotes to mongo-direct, or fails closed withParse::Query::MongoDirectRequiredwhen direct Mongo is unavailable; unscoped callers can still opt out to REST withmongo_direct: false.
Additional hardening
- FIXED: Request/response body logging now redacts credentials. At
:debuglevel the logging middleware emitted login/signup request bodies (cleartextpassword) and auth response bodies (sessionToken,authData, MFA secrets); the body path now runs through the sameBodyBuilder.redactscrubber the header path already used, before truncation. - FIXED: The
_UserREST endpoints (fetch_user/update_user/delete_user) now validate theobjectIdagainstParse::API::PathSegment.object_id!before interpolating it into the path, matching the object endpoints. A crafted objectId can no longer traverse to a different endpoint on a subsequent request. - CHANGED:
$sessionToken/$session_token(the camelCase forms of the session-token column) are now inDENIED_FIELD_REFS, so they cannot be laundered through a$-field reference in a pipeline. - IMPROVED: The internal-collection floor (
_SCHEMA/_Hooks/_GlobalConfig/_Audit/ ...) is now enforced unconditionally on every$lookup/$graphLookup/$unionWithjoin target inParse::ACLScope, not only when lookup-rewriting runs. This closes a defense-in-depth gap where an internal class whose CLP lookup returned no policy could otherwise have been joinable on the direct path. - IMPROVED: When the MCP agent server is started on an unauthenticated loopback bind with no Origin/custom-header gate configured, it now defaults to a loopback-only Origin policy. A browser DNS-rebinding attack against
127.0.0.1carries a non-loopbackOriginand is refused; native clients (which send noOrigin) and local browser UIs are unaffected. A one-time warning points operators atMCP_API_KEY/allowed_origins:/require_custom_header:for routable deployments.
Commit: a68b53d
Author: Adrian Curtin
Date: June 10, 2026