Repository navigation
v5.8.2 - Batch Credentials, Regex Checks, Agent and Webhook Trust
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 /batchunder the outer call's credentials and ignores per-sub-request headers.Array#save,Array#destroy,Parse.batch, andParse::Object.transactionsent everything through the default client, so a write built for a user session (a class bound toParse.client.become(token), or aParse::Requestwithsession_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 clientbatch_requestis called on) is used for every request; an object's class client applies only to implicit batches. Ause_master_key:that is not exactlytrueorfalseis treated as unset, and a session that resolves to no token fails closed. - FIXED: A transaction that mixes credentials raises
Parse::BatchOperation::MixedAuthorityErrorbefore 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:), andParse::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, andrefresh_scope!are consistent under concurrency, sub-agents reuse a parent scope only from a locked snapshot, andstop_impersonating!is a no-op unless the agent is impersonating. - FIXED:
Parse::AgentandParse::Clientinspect,to_s,pp,as_json,to_json, andto_yamlprint redacted summaries, and both refuseMarshal.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 coverssession_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 clientParse.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
$regexin a where hash (including:field.eq,:field.not,$elemMatch,:orbranches, andRegexpvalues under$not/$in) and every mongo-direct filter and pipeline (results_direct,count_direct,Query#aggregate,Parse::MongoDB.aggregateandfind, Atlas and vector filters, LiveQuerywhere, the agentaggregatetool) now go throughParse::RegexSecurity. Before, only the regex constraints were checked and direct paths capped only the length. - FIXED:
Parse::RegexSecurityparses 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_msof 5000 ms (Parse::PipelineSecurity.default_regex_max_time_ms; an explicitmax_time_ms:wins). For REST queries, set Parse Server'sdatabaseOptions.maxTimeMS. - FIXED:
$regexMatch/$regexFind/$regexFindAllin caller pipelines require a literal pattern, andParse::MongoDB.findraisesParse::ACLScope::ACLRequiredinsideParse.without_master_key.
Webhook master trust requires authenticated ingress
- FIXED: A webhook request's
masterflag is trusted only when the webhook key matched or a configured signature verified. UnderParse::Webhooks.allow_unauthenticatedwith no signature, a body claiming"master": trueno longer passes master-only field guards, ACL owner resolution, handlerpayload.master?checks, or the Ruby-initiated callback dedup, so a forged request cannot skip abefore_savethat rejects it. Only a JSONtruecounts 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 passesrequire_https), and the TLS guard also refusesverify_mode: VERIFY_NONE,verify_hostname: false,Faraday::SSLOptions, andFaraday::ConnectionOptionswith verification off. - FIXED: LiveQuery URLs are validated in the client constructor as well as at setup; plaintext
ws://to a routable host is refused withoutallow_insecure. CHANGED:https://is used aswss://and a loopbackhttp://asws://, with a deprecation warning. - FIXED: Hydrated
Parse::FileURLs are read the way a browser reads them and checked againsttrusted_url_hosts, including uppercase schemes, leading whitespace or control characters, scheme-relative values, and malformed forms;javascript:anddata:URLs are untrusted.Parse::File.trust_legacy_tfss_on_any_host = falselocks downtfss-URLs. - FIXED:
Parse::Response#to_sand 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.#resultis 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 blanksession:raisesArgumentError. - Custom agent handlers that test
agent.acl_scopefor truthiness should useagent.acl_scope?;acl_scoperaisesUnresolvedIdentityfor an unresolved session agent. Marshaling aParse::ClientorParse::AgentraisesTypeError. - Queries that set
cache: truewith a session token no longer get cache hits; setcache_session_requeststotrueto restore the old behavior. - A regex the check rejects now raises (
ArgumentErroron REST,Parse::PipelineSecurity::Erroron direct paths).$optionsacceptsi,m,s, andu; the extended flagxraises. A mongo-direct read with a non-literal regex and nomax_time_mstimes out after 5 seconds. - Under
allow_unauthenticatedwith 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_KEYon Parse Server andParse::Webhooks.key) or a signing secret. Parse::Response#to_sreplaces credential values with"[FILTERED]"; read#resultfor raw data.
Commit: 1221f57
Author: Adrian Curtin
Date: October 7, 2026