Skip to content

v5.8.4 - Agent and Atlas Scoping, Regex Compilation, Transport Hardening

Latest

Choose a tag to compare

@AdrianCurtin AdrianCurtin released this 26 Oct 16:50
9cb8c14

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_search applies the agent's classes: allowlist, the CLP find check for scoped agents, and the per-agent filters:, 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 as PublicState addresses that column; an undeclared field raises Parse::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/updated notifications reported writes in other tenants and in filtered-out rows.
  • FIXED: An agent $relatedTo must 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 raises ConstraintSecurityError (:invalid_object_id, :invalid_key, :duplicate_key). A __type value 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: and impersonate refusals apply.
  • NEW: A loopback-bound MCP server refuses requests whose Host header is not a loopback host (the MCP specification's DNS-rebinding defense). allowed_hosts: on Parse::Agent::MCPServer and MCPRackApp lists 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*, *token on _User) and queryString field names, including Lucene-escaped ones (_hashed\_password:a*), are refused too. On a dynamically mapped index, a wildcard operator over _perishable_token let a caller count matches per prefix and recover a password-reset token. Refusals raise Parse::CLPScope::Denied before the pipeline runs.
  • FIXED: $jsonSchema is refused at any depth in mongo-direct filters and pipelines, alongside $where and $function. Its properties named 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 its pattern skipped 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_OwnerRef column on every path. A constraint keyed by OwnerRef reached a missing column, where $ne or $exists: false matched 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 $match on 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 every Parse::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 inside Parse.with_session as it already was inside Parse.without_master_key. A Parse.with_master_key block entered inside the session block still allows it.

Caller regular expressions are no longer compiled by Ruby

  • FIXED: Parse::RegexSecurity no longer passes any part of a caller's $regex to Regexp.new. Its separator check compiled character classes, braced escapes, and \Q...\E atoms (which Ruby's engine reads as live syntax) from agent and MCP arguments, :field.like / :field.regex values, raw where hashes, 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::Client refuses an absolute or protocol-relative request path (https://host/x in any case, //host, \\host) with ArgumentError before 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 with ArgumentError, since anyone able to write to that backend could run code on each cache read. Use Parse::Cache::Redis (or a redis:// URL string) or an in-process store, or pass allow_marshal_cache: true to accept the risk; the embedding cache takes allow_marshal: true.
  • FIXED: Credentials embedded in the server URL are shown as [FILTERED] in inspect, as_json, and warnings. PARSE_REQUIRE_HTTPS accepts true, 1, or yes in 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_KEY no longer hides PARSE_WEBHOOK_KEY. The webhook handler refuses a declared Content-Length over 1 MB before reading and reads at most 1 MB plus one byte.
  • FIXED: Remote Parse::File fetches 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 _User and _Session have sessionToken, authData, and other credential columns removed before hydration. On a Parse Server affected by GHSA-349r-988w-x3jp, an $or subscription 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_session documents 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 { } (with keep_restrictions: true to never lift a restriction) runs worker code without inherited identity.

GraphQL, MFA, and cursors stop exposing credentials

  • FIXED: Parse::GraphQL::TypeGenerator never emits credential fields (session_token, auth_data, password, device_token, and the internal field list); field resolution bypassed as_json redaction, so a generated Session type served bearer tokens. include_sensitive: opts a field back in.
  • NEW: The generator takes an explicit field policy, exclude_fields: and protected_fields: (a Hash per class, or :fetch to load each class's public protectedFields and raise ProtectedFieldsUnavailable if 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::Cursor state can be HMAC-SHA256 signed (Parse::Cursor.signing_secret= or secret:), and deserialize(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: pass trusted: true to 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:) and Parse::MFA.verify_timestamp let an application refuse a code used again within the drift window.
  • CHANGED: The MFA, MCP, and Parse::File documentation 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 with Vips.block_untrusted(true)).

Dependencies, CI, and release workflow

  • CHANGED: The gemspec requires faraday 2.14.3 or later (CVE-2026-54297, CVE-2026-33637, GHSA-33mh-2634-fwr2) and rack 2.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 optional mongo guidance is 2.26 or later (CVE-2026-88030) and graphql 2.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-ruby 1.334.0, pins every action to a commit SHA, and runs bundle-audit on 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 raises ArgumentError at Parse.setup.
  • Parse::Client#get / post / put / delete and connected? raise ArgumentError for an absolute or protocol-relative path.
  • A loopback-bound MCP server behind a reverse proxy that keeps the public Host needs that host in allowed_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 _User or _Session with no fields: is refused.
  • A pipeline or filter using $jsonSchema raises Parse::PipelineSecurity::Error (or Parse::MongoDB::DeniedOperator).
  • An agent call_method handler under a session-scoped agent runs inside Parse.with_session, so Parse::MongoDB.find there raises Parse::ACLScope::ACLRequired. Use Parse::Query#results_direct, which applies the ambient session, Parse::MongoDB.aggregate with session_token:, or a Parse.with_master_key block for a deliberate master read.
  • A per-agent filter naming a field the searched class does not declare makes semantic_search raise Parse::Agent::ValidationError.
  • An application subscribed to its own _User through LiveQuery no longer receives its sessionToken in events.
  • Parse::Cursor secrets that are blank, shorter than 32 bytes, or not Strings raise ArgumentError; mfa_required?(nil) raises instead of returning false.
  • faraday below 2.14.3 and rack below 2.2.23 no longer satisfy the gemspec. rack 3.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