Skip to content

v5.8.2 - Batch Credentials, Regex Checks, Agent and Webhook Trust

Choose a tag to compare

@AdrianCurtin AdrianCurtin released this 08 Oct 00:58
· 5 commits to main since this release
1221f57

Security and Correctness Release

A security and correctness patch scoped from the 5.8.1 backlog survey: batch writes keep each write's own credentials, agents fail closed when their session cannot be resolved, session reads no longer come from the response cache after revocation, every regex path goes through a parser-based ReDoS check, and webhook master trust requires authenticated ingress, plus transport and redaction hardening found in review. There are no new defaults beyond what the fixes need; intentional master-key and analytics access is unchanged, and outcomes that change are listed under Behavior Notes.

Changes

Batched writes run under the credentials they were built for

  • FIXED: Parse Server runs every sub-request of POST /batch under the outer call's credentials and ignores per-sub-request headers. Array#save, Array#destroy, Parse.batch, and Parse::Object.transaction sent everything through the default client, so a write built for a user session (a class bound to Parse.client.become(token), or a Parse::Request with session_token: / use_master_key: false) ran with the master key and bypassed ACLs and CLPs. Each write now resolves its credentials exactly as a single request would (options, X-Parse-Session-Token, the master-key suppression header, the class client, the ambient session), and a batch with mixed credentials goes out as separate calls with responses in request order.
  • FIXED: A client chosen explicitly (BatchOperation#client=, or the client batch_request is called on) is used for every request; an object's class client applies only to implicit batches. A use_master_key: that is not exactly true or false is treated as unset, and a session that resolves to no token fails closed.
  • FIXED: A transaction that mixes credentials raises Parse::BatchOperation::MixedAuthorityError before anything is sent, because Parse Server runs a transaction under one credential. Clients are compared by credentials, not object identity, so identically configured clients still share one transaction.
  • NEW: Array#save(session:), Array#destroy(session:), and Parse::Object.transaction(session:) send every write in the call as the given user.

Agents fail closed when their session cannot be resolved

  • FIXED: A session-token agent whose token could not be resolved (for example, Parse Server unreachable) is no longer treated as master-key posture by the SDK-side permission checks. It retries on first use, outside its scope lock and with a 5-second backoff, and otherwise refuses with Parse::Agent::UnresolvedIdentity (:access_denied). A token Parse Server rejected is not retried.
  • FIXED: Scope resolution, impersonate, and refresh_scope! are consistent under concurrency, sub-agents reuse a parent scope only from a locked snapshot, and stop_impersonating! is a no-op unless the agent is impersonating.
  • FIXED: Parse::Agent and Parse::Client inspect, to_s, pp, as_json, to_json, and to_yaml print redacted summaries, and both refuse Marshal.dump, so tokens and keys no longer reach logs, error trackers, or marshaled caches.

Revoked sessions stop reading cached rows

  • FIXED: Reads made with a session token bypass the response cache, so a revoked session (logout, Parse::Session#destroy, logout_all!, a password change) or a user who lost access no longer reads cached rows until the entry expires. This covers session_token:, Parse.with_session, and session-bound clients; master-key and anonymous reads cache as before.
  • NEW: Parse::Middleware::Caching.cache_session_requests = true, or per client Parse.setup(cache_session_requests: true), opts back in for apps that accept revocation bounded by the cache TTL.
  • FIXED: Array#destroy(session:) on sessions looks rows up as that user; a successful delete still invalidates the recorded owner, while denied or not-found deletes forget nothing.

Every regex goes through a parser-based ReDoS check

  • FIXED: A raw $regex in a where hash (including :field.eq, :field.not, $elemMatch, :or branches, and Regexp values under $not / $in) and every mongo-direct filter and pipeline (results_direct, count_direct, Query#aggregate, Parse::MongoDB.aggregate and find, Atlas and vector filters, LiveQuery where, the agent aggregate tool) now go through Parse::RegexSecurity. Before, only the regex constraints were checked and direct paths capped only the length.
  • FIXED: Parse::RegexSecurity parses each pattern instead of matching substrings. It refuses groups repeated more than once that contain a variable-count quantifier, an alternation, or a backreference ((a+)+, (a|aa)+$, ^(a?){100}a{100}$, (?i)^(a|Aa)+$), subroutine calls, long runs of unbounded quantifiers, comments, and extended mode. It accepts single-atom repeats (^.{1,255}$), simple lookarounds (^(?!test)), non-overlapping alternations (^(foo|bar)+$), and separator runs (^[a-z0-9]+(?:-[a-z0-9]+)*$).
  • IMPROVED: Mongo-direct reads carrying a non-literal regex get a default server-side max_time_ms of 5000 ms (Parse::PipelineSecurity.default_regex_max_time_ms; an explicit max_time_ms: wins). For REST queries, set Parse Server's databaseOptions.maxTimeMS.
  • FIXED: $regexMatch / $regexFind / $regexFindAll in caller pipelines require a literal pattern, and Parse::MongoDB.find raises Parse::ACLScope::ACLRequired inside Parse.without_master_key.

Webhook master trust requires authenticated ingress

  • FIXED: A webhook request's master flag is trusted only when the webhook key matched or a configured signature verified. Under Parse::Webhooks.allow_unauthenticated with no signature, a body claiming "master": true no longer passes master-only field guards, ACL owner resolution, handler payload.master? checks, or the Ruby-initiated callback dedup, so a forged request cannot skip a before_save that rejects it. Only a JSON true counts as master.

Transport, File URL, and redaction hardening

  • FIXED: A whitespace-only or Unicode-space MCP API key counts as no key instead of disabling authentication on a public bind; a short key on a public bind warns.
  • FIXED: Scheme checks are case-insensitive (HTTP:// no longer passes require_https), and the TLS guard also refuses verify_mode: VERIFY_NONE, verify_hostname: false, Faraday::SSLOptions, and Faraday::ConnectionOptions with verification off.
  • FIXED: LiveQuery URLs are validated in the client constructor as well as at setup; plaintext ws:// to a routable host is refused without allow_insecure. CHANGED: https:// is used as wss:// and a loopback http:// as ws://, with a deprecation warning.
  • FIXED: Hydrated Parse::File URLs are read the way a browser reads them and checked against trusted_url_hosts, including uppercase schemes, leading whitespace or control characters, scheme-relative values, and malformed forms; javascript: and data: URLs are untrusted. Parse::File.trust_legacy_tfss_on_any_host = false locks down tfss- URLs.
  • FIXED: Parse::Response#to_s and log redaction cover MFA recovery codes and secrets, storage-form credential columns, header-shaped text, and whole quoted values, and run on string values so the output stays valid JSON. #result is unchanged.

Behavior Notes

  • A batch or transaction of objects whose class is bound to a session client now runs as that user, as a single save does. A transaction mixing credentials raises MixedAuthorityError; a non-transactional batch with mixed credentials is sent as several calls. A blank session: raises ArgumentError.
  • Custom agent handlers that test agent.acl_scope for truthiness should use agent.acl_scope?; acl_scope raises UnresolvedIdentity for an unresolved session agent. Marshaling a Parse::Client or Parse::Agent raises TypeError.
  • Queries that set cache: true with a session token no longer get cache hits; set cache_session_requests to true to restore the old behavior.
  • A regex the check rejects now raises (ArgumentError on REST, Parse::PipelineSecurity::Error on direct paths). $options accepts i, m, s, and u; the extended flag x raises. A mongo-direct read with a non-literal regex and no max_time_ms times out after 5 seconds.
  • Under allow_unauthenticated with no signing secret, payload.master? is false even for master-key requests Parse Server forwards, and SDK-initiated saves run their model callbacks twice. Configure a webhook key (PARSE_SERVER_WEBHOOK_KEY on Parse Server and Parse::Webhooks.key) or a signing secret.
  • Parse::Response#to_s replaces credential values with "[FILTERED]"; read #result for raw data.

Commit: 1221f57
Author: Adrian Curtin
Date: October 7, 2026