Repository navigation
Security Patch
A security release across the agent, MCP, Atlas Search, mongo-direct, transport, cache, webhook, LiveQuery, GraphQL, MFA, and cursor surfaces: agent class policy and per-agent filters now apply to semantic_search and MCP subscriptions, Parse Server's internal columns are refused across every search and pipeline entry point, caller regex text is no longer compiled by Ruby, scoped reads decide on one schema snapshot, and the faraday and rack floors rise, with a load-time warning on vulnerable Rack 3.x releases.
Breaking Changes
None in the public API. Several insecure inputs that used to work now raise; they are listed under Behavior Notes.
Changes
Agents and MCP enforce their class and tenant policy on every path
- FIXED:
semantic_searchapplies the agent'sclasses:allowlist, the CLPfindcheck for scoped agents, and the per-agentfilters:, as every other read tool does. Before, it checked only the global hidden flag and the tenant scope, so an agent limited to one class could search any other searchable class, including rows its per-agent filter excludes. The per-agent filter is ANDed after the caller's filter is checked (and with a retrieval profile's hybrid filters), and its fields resolve through declared properties, so a declared server name such asPublicStateaddresses that column; an undeclared field raisesParse::Agent::ValidationError. - FIXED: MCP resource subscriptions apply the agent's tenant scope and per-agent filter as the LiveQuery constraint. Before, a subscription covered the whole class, so
resources/updatednotifications reported writes in other tenants and in filtered-out rows. - FIXED: An agent
$relatedTomust name the owning object with a plain objectId and the relation with a field name. An operator objectId ({ "$exists" => true }) matches every owning object without its ACL on Parse Server below 9.10.5 (GHSA-58m4-cqvw-4vgh); it now raisesConstraintSecurityError(:invalid_object_id,:invalid_key,:duplicate_key). A__typevalue that also carries an operator key is refused (:operator_in_parse_type) instead of skipping validation. - FIXED: An agent on a client whose master key is empty or whitespace is treated as having no master key, as the HTTP layer already did, so the
acl_user:/acl_role:andimpersonaterefusals apply. - NEW: A loopback-bound MCP server refuses requests whose
Hostheader is not a loopback host (the MCP specification's DNS-rebinding defense).allowed_hosts:onParse::Agent::MCPServerandMCPRackApplists additional hosts.
Parse Server internal columns can no longer be searched, sorted, or matched
- FIXED: Atlas Search refuses
_hashed_password,_perishable_token,_email_verify_token,_session_token,_auth_data_*,_rperm, and the rest of the internal field list as a search path, highlight, sort key, autocomplete field, or facet path, for every caller including master, and the native hybrid search applies the same check to its lexical branch. Globs that reach those columns (_perishable_tok*,*tokenon_User) andqueryStringfield names, including Lucene-escaped ones (_hashed\_password:a*), are refused too. On a dynamically mapped index, awildcardoperator over_perishable_tokenlet a caller count matches per prefix and recover a password-reset token. Refusals raiseParse::CLPScope::Deniedbefore the pipeline runs. - FIXED:
$jsonSchemais refused at any depth in mongo-direct filters and pipelines, alongside$whereand$function. Itspropertiesnamed fields out of reach of the protectedFields key checks, so{ $jsonSchema: { properties: { email: { pattern: "^a" } } } }read a protected field one character at a time, and itspatternskipped the SDK's regex checks. - FIXED: The protectedFields and internal-field checks cover
$setWindowFields,$densify,$fill,$bucket,$bucketAuto,$sortByCount, and a top-level$search/$searchMeta, where a scoped pipeline could sort or partition on a protected field and read its order. - FIXED: The mongo-direct field conversion resolves a pointer's declared server name (
belongs_to :owner_ref, field: "OwnerRef") to its_p_OwnerRefcolumn on every path. A constraint keyed byOwnerRefreached a missing column, where$neor$exists: falsematched every row.
Scoped reads decide on one schema snapshot through the requesting app
- FIXED: The mongo-direct protected-field check read the class schema through the default client and treated an unresolvable schema as having no protected fields, while the CLP gate and the strip read it separately through the requesting app's client. If the fetch failed and then succeeded between the check and the gate, a
$matchon a protected field ran; in a multi-app process whose default app had no protectedFields on the class,$group: { _id: "$ssn" }returned the protected value. The check now uses the requesting app's client, refuses a scoped caller when the schema cannot be resolved, and everyParse::MongoDB.aggregate, Atlas Search, vector search, and hybrid search call reads one snapshot for all its steps. - FIXED:
Parse::MongoDB.find, which reads raw documents with master authority, is refused insideParse.with_sessionas it already was insideParse.without_master_key. AParse.with_master_keyblock entered inside the session block still allows it.
Caller regular expressions are no longer compiled by Ruby
- FIXED:
Parse::RegexSecurityno longer passes any part of a caller's$regextoRegexp.new. Its separator check compiled character classes, braced escapes, and\Q...\Eatoms (which Ruby's engine reads as live syntax) from agent and MCP arguments,:field.like/:field.regexvalues, rawwherehashes, and mongo-direct and Atlas filters. Ruby's regular-expression compiler has memory-safety bugs fixed in Ruby 3.3.13, 3.4.12, and 4.0.9 (GitLab-assigned CVE-2026-89078, CVE-2026-93577, CVE-2026-97229, CVE-2026-110264). Membership is now decided from the validator's own parse; anything it cannot evaluate counts as a possible match, which refuses the pattern. Patterns still reach MongoDB as strings and are compiled only by its PCRE engine.
Transport, caching, and webhooks refuse unsafe input
- FIXED:
Parse::Clientrefuses an absolute or protocol-relative request path (https://host/xin any case,//host,\\host) withArgumentErrorbefore sending, so a caller-supplied path cannot send the master key or a session token to another host. - CHANGED: A
cache:store that deserializes with Marshal (Moneta's default) over an external backend is refused withArgumentError, since anyone able to write to that backend could run code on each cache read. UseParse::Cache::Redis(or aredis://URL string) or an in-process store, or passallow_marshal_cache: trueto accept the risk; the embedding cache takesallow_marshal: true. - FIXED: Credentials embedded in the server URL are shown as
[FILTERED]ininspect,as_json, and warnings.PARSE_REQUIRE_HTTPSacceptstrue,1, oryesin any case. - FIXED: A whitespace-only webhook signing secret counts as unset instead of serving as a guessable HMAC key that grants master trust, and a blank
PARSE_SERVER_WEBHOOK_KEYno longer hidesPARSE_WEBHOOK_KEY. The webhook handler refuses a declaredContent-Lengthover 1 MB before reading and reads at most 1 MB plus one byte. - FIXED: Remote
Parse::Filefetches also refuse the NAT64 and 6to4 IPv6 ranges and IPv4-compatible addresses, which can embed an internal IPv4 address.
LiveQuery and threads keep identity where it belongs
- FIXED: LiveQuery events for
_Userand_SessionhavesessionToken,authData, and other credential columns removed before hydration. On a Parse Server affected by GHSA-349r-988w-x3jp, an$orsubscription could receive other users' events carrying their session tokens. - FIXED: LiveQuery's event, reader, reconnect, and health-check threads start without the caller's ambient identity, so callbacks no longer run with a session token inherited from whichever block started the client, and a thread started from restricted code keeps the master key suppressed.
- NEW:
Parse.with_sessiondocuments its identity lifetime: the token lasts for the block on the current fiber and is restored on exit, so reused request threads carry nothing between requests, while threads created inside the block inherit it.Parse.without_ambient_identity { }(withkeep_restrictions: trueto never lift a restriction) runs worker code without inherited identity.
GraphQL, MFA, and cursors stop exposing credentials
- FIXED:
Parse::GraphQL::TypeGeneratornever emits credential fields (session_token,auth_data,password,device_token, and the internal field list); field resolution bypassedas_jsonredaction, so a generatedSessiontype served bearer tokens.include_sensitive:opts a field back in. - NEW: The generator takes an explicit field policy,
exclude_fields:andprotected_fields:(a Hash per class, or:fetchto load each class's public protectedFields and raiseProtectedFieldsUnavailableif it cannot), so the same models always produce the same types. Generated types are a schema shape, not an authorization layer; resolvers must still enforce ACL, CLP, and protectedFields for the requesting user. - NEW:
Parse::Cursorstate can be HMAC-SHA256 signed (Parse::Cursor.signing_secret=orsecret:), anddeserialize(json, expected_class:)refuses another class. Secrets must be Strings of at least 32 bytes, generated from a cryptographic random source (SecureRandom.hex(32)). Unsigned state is for trusted persistence: passtrusted: trueto load it silently, which never bypasses a configured secret. Without the flag it loads with a one-time warning in this release. - FIXED:
mfa_required?requires a String username; a Hash became a query operator run under the master key. NEW:Parse::MFA.verify(secret, code, after:)andParse::MFA.verify_timestamplet an application refuse a code used again within the drift window. - CHANGED: The MFA, MCP, and
Parse::Filedocumentation states that Parse Server is the MFA enforcement boundary, that MCP rate limits must be enforced per client at the deployment layer, and that a file built from a URL records the remote server's attacker-controllable Content-Type (decoders such as libvips should run withVips.block_untrusted(true)).
Dependencies, CI, and release workflow
- CHANGED: The gemspec requires
faraday2.14.3 or later (CVE-2026-54297, CVE-2026-33637, GHSA-33mh-2634-fwr2) andrack2.2.23 or later. The 2026-04 rack advisories are fixed per branch (2.2.23, 3.1.21, 3.2.6, with no fixed 3.0 release), so loading the SDK on a vulnerable Rack 3.x release prints a one-time warning naming the fixed release; Rack 2.2 applications remain supported. The optionalmongoguidance is 2.26 or later (CVE-2026-88030) andgraphql2.6.9 or later. - CHANGED: The integration stack runs Parse Server 9.10.7 and Atlas Local 8.0.34. CI installs the patched Ruby 3.3.13, 3.4.12, and 4.0.9 through
ruby/setup-ruby1.334.0, pins every action to a commit SHA, and runsbundle-auditon every push. The Ruby 3.2 leg remains for legacy compatibility; 3.2 is end of life and has no release with the regular-expression compiler fixes. - FIXED: The release workflow waits for Gem Tests on the release commit to finish before judging it, and fails if they do not, instead of failing a release published right after its merge.
Behavior Notes
- A
cache:store with Moneta's default Marshal serializer over a file or network backend raisesArgumentErroratParse.setup. Parse::Client#get/post/put/deleteandconnected?raiseArgumentErrorfor an absolute or protocol-relative path.- A loopback-bound MCP server behind a reverse proxy that keeps the public
Hostneeds that host inallowed_hosts:. - A scoped mongo-direct, Atlas Search, or vector search call on a class whose schema cannot be resolved is refused before it runs. A non-master Atlas Search over
_Useror_Sessionwith nofields:is refused. - A pipeline or filter using
$jsonSchemaraisesParse::PipelineSecurity::Error(orParse::MongoDB::DeniedOperator). - An agent
call_methodhandler under a session-scoped agent runs insideParse.with_session, soParse::MongoDB.findthere raisesParse::ACLScope::ACLRequired. UseParse::Query#results_direct, which applies the ambient session,Parse::MongoDB.aggregatewithsession_token:, or aParse.with_master_keyblock for a deliberate master read. - A per-agent filter naming a field the searched class does not declare makes
semantic_searchraiseParse::Agent::ValidationError. - An application subscribed to its own
_Userthrough LiveQuery no longer receives itssessionTokenin events. Parse::Cursorsecrets that are blank, shorter than 32 bytes, or not Strings raiseArgumentError;mfa_required?(nil)raises instead of returning false.faradaybelow 2.14.3 andrackbelow 2.2.23 no longer satisfy the gemspec.rack3.0.x, 3.1.0 to 3.1.20, and 3.2.0 to 3.2.5 still install and print a load-time warning.- Applications that compile regular expressions from untrusted input themselves should run Ruby 3.3.13, 3.4.12, or 4.0.9 or later.
Commit: 9cb8c14
Author: Adrian Curtin
Date: October 26, 2026