Skip to content

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

Merged
AdrianCurtin merged 20 commits into
mainfrom
release/5.8.2
Oct 8, 2026
Merged

AdrianCurtin merged 20 commits into
mainfrom
release/5.8.2

Conversation

@AdrianCurtin

@AdrianCurtin AdrianCurtin commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Release 5.8.2: a security and correctness patch scoped from the 5.8.1 backlog survey. It fixes batch writes that ran with the master key, agents that fell back to master posture when their session could not be resolved, session reads served from the response cache after revocation, ReDoS checks that most regex paths skipped, and webhook master trust on unauthenticated ingress, plus transport and redaction gaps found in review. No new features beyond the session: arguments and the cache opt-in the fixes need; intentional master-key and analytics access is unchanged.

Batches and transactions

  • Parse Server runs every sub-request of POST /batch under the outer call's credentials. Array#save, Array#destroy, Parse.batch, and transaction sent everything through the default client, so writes built for a user session ran as master. Each write now goes out under the credentials it would have as a single request (options, session header, master-key suppression header, class client, ambient session), grouped by a credential fingerprint.
  • A client chosen explicitly (BatchOperation#client= or the batch_request receiver) is used for every request. Mixed-credential non-transactional batches are split per group with responses in order; a transaction that mixes credentials raises MixedAuthorityError before anything is sent.
  • use_master_key counts only when exactly true or false, and a session that resolves to no token fails closed. Array#save, Array#destroy, and transaction accept session:.

Agents

  • A session-token agent whose token cannot be resolved no longer runs the SDK-side permission checks as master. It retries on first use (outside the scope lock, with a 5-second backoff) and otherwise raises Parse::Agent::UnresolvedIdentity.
  • Scope reads, impersonate, and refresh_scope! are consistent under concurrency; sub-agents reuse a parent scope only from a locked snapshot; stop_impersonating! is a no-op unless impersonating.
  • Agent and Client inspect, to_s, as_json, to_json, and to_yaml are redacted, and both refuse Marshal.dump.

Response cache

  • Reads made with a session token bypass the response cache by default, so a revoked session or a user who lost access no longer reads cached rows until expiry. Opt back in with cache_session_requests (globally or per client). Master-key and anonymous caching is unchanged.

Regex and direct reads

  • Raw $regex in REST where hashes and every mongo-direct filter and pipeline now go through Parse::RegexSecurity, which parses each pattern instead of matching substrings: it refuses nested, alternation, optional, and subroutine repeats, long runs of wide quantifiers, comments, and extended mode, and accepts single-atom repeats, simple lookarounds, and non-overlapping alternations and separator runs.
  • Mongo-direct reads carrying a non-literal regex get a default 5-second max_time_ms. Parse::MongoDB.find raises ACLRequired inside without_master_key.

Webhooks

  • A webhook master claim is trusted only on authenticated ingress (webhook key or verified signature). Forged claims under allow_unauthenticated no longer pass master-only guards, ACL owner resolution, handler checks, or callback dedup.

Transport and redaction

  • Blank and whitespace MCP API keys count as no key. Scheme checks are case-insensitive; the TLS guard covers verify_mode, verify_hostname, SSLOptions, and ConnectionOptions. LiveQuery URLs are validated, and http(s):// maps to ws(s):// with a deprecation warning.
  • Hydrated Parse::File URLs are normalized the way a browser reads them and checked against trusted_url_hosts, including scheme-relative and non-http(s) values.
  • Redaction covers MFA recovery codes, storage-form credential columns, header-shaped text, and whole quoted values, and runs on string values so Response#to_s stays valid JSON.

Behavior changes for upgrading apps

CHANGELOG.md lists each under Behavior Notes. The ones most likely to need changes:

  • Batches of objects whose class is bound to a session client now run as that user; mixed-credential transactions raise.
  • Custom agent handlers that test agent.acl_scope for truthiness should use agent.acl_scope?.
  • Session-token queries with cache: true no longer hit the response cache.
  • Regex patterns the parser refuses now raise; $options accepts i, m, s, u.
  • Under allow_unauthenticated with no signing secret, payload.master? is false; configure a webhook key or signing secret.
  • Response#to_s filters credential values; read #result for raw data.

Testing

  • bundle exec rake test:unit passes.
  • Integration files for every changed area pass against Parse Server 9.10.3 and MongoDB 9.0.2, including new tests for batch credentials and the webhook suite running with a configured webhook key. The test stack now sets PARSE_SERVER_WEBHOOK_KEY.
  • Each fix has a regression test that fails on the previous code; batch credential parity is checked against the real auth middleware.

A whitespace-only MCP API key now counts as no key, so a public bind
refuses to start and requests never authenticate against a blank key.
HTTPS and WebSocket scheme checks parse the URL, so HTTP://, HTTPS://
with TLS verification off, and WS:// are caught. LiveQuery validates an
explicit or configured URL like a derived one. Parse::Response#inspect
shows only status and shape, and to_s filters credential fields.
A session-token agent whose token did not resolve at construction had a
nil scope, which the SDK-side CLP check read as master. It now retries
resolution on first use and raises Parse::Agent::UnresolvedIdentity if
it still cannot resolve. Agent#inspect, to_s, and pp print a redacted
summary without the token, client keys, conversation, or last
request/response.
A cached answer was served without contacting Parse Server, so a revoked
session, or a user who lost role or row access, kept reading cached rows
until the entry expired. The caching middleware, which sees the final
session for explicit, ambient, and client-bound sessions, now bypasses
session reads unless cache_session_requests is enabled. Master-key and
anonymous reads cache as before.
Raw $regex in REST where hashes (eq, not, $elemMatch, OR branches) and
every mongo-direct filter and pipeline (Regexp, BSON regex, $regexMatch
family) now run the ReDoS check, with $options limited to imxsu. SDK-built
escaped literal patterns are always accepted. The agent constraint
translator applies the check too. Parse::MongoDB.find raises ACLRequired
inside without_master_key; indexes stays available as metadata.
Under allow_unauthenticated with no signature, a body claiming master no
longer passes master-only field guards, ACL owner resolution, or handler
payload.master? checks. payload.claimed_master? keeps the raw claim for
Ruby-initiated callback dedup, and the string "false" is not master.
Parse Server runs every /batch sub-request under the outer call's
credentials, and the SDK sent all batched writes through the default
client, so writes built for a user session ran as master. Requests now
carry their client and explicit session options; a batch with mixed
credentials is sent as separate calls in request order, and a
transaction mixing credentials raises MixedAuthorityError before sending.
Array#save, Array#destroy, and transaction accept session:.
Bump the version and add the 5.8.2 CHANGELOG entry.
… fingerprint

Batched requests resolve credentials exactly as a single request does:
session_token/use_master_key options, the X-Parse-Session-Token header,
and the master-key suppression header, which wins over use_master_key.
Direct Parse::Client#batch_request calls get the same grouping and
mixed-transaction refusal. Clients are compared by a credential
fingerprint instead of object identity, so classes holding identically
configured clients after a second Parse.setup share one transaction.
Agent and Parse::Client as_json, to_json, and to_yaml return a redacted
summary. A sub-agent holding its parent's token reuses the parent's
scope, and a failed different token reports UnresolvedIdentity rather
than a widening. Scope resolution, impersonate, and refresh_scope! share
one reentrant lock; acl_scope checks and reads under it and
acl_permission_strings never returns nil for a session agent. Failed
lazy resolution backs off for 5 seconds, and an invalid token gets its
own message. Client also gains the TLS, loopback, LiveQuery URL, and
per-client cache_session_requests changes from the transport review.
Parse::File trusted-host checks parse the scheme and treat malformed
http(s) URLs as untrusted. Redaction covers MFA recovery codes, secrets,
and storage-form credential columns, and Response error text is redacted
and escaped. The TLS guard refuses verify_mode/verify_hostname and
SSLOptions forms. MCP keys strip Unicode whitespace and warn when short
on a public bind. LiveQuery maps http/https to ws/wss with a warning.
Webhook endpoints warn on cleartext http, and the cache bypass event
carries cache_tenant.
RegexSecurity tokenizes each caller pattern and refuses groups repeated
more than once whose body has a variable-count quantifier (including ?),
an alternation, or a backreference, plus (?# comments and extended mode.
Single-atom repeats and simple lookarounds are accepted. Escaped
literals get a longer cap, $regexMatch-family operands must be literal,
and one options allowlist (imsu) is shared by every gate.
A forged _RB_ request id with master: true on unauthenticated ingress no
longer skips model callbacks, including a rejecting before_save.
claimed_master? is diagnostic only.
…options

A client chosen with BatchOperation#client= or as the batch_request
receiver is used for every request, as for a single request; an object's
class client applies only to implicit batches. use_master_key counts
only when exactly true or false, a session that resolves to no token
fails closed, and narrowing call options override a request's own
use_master_key: true. Fingerprints use a per-process HMAC key and are
memoized per submit; transaction(session:) ignores master keys and bound
tokens when grouping. Session deletes look up rows as the deleting user,
and a successful delete of a row that user cannot read still invalidates
its recorded owner or falls back to the rate-limited reset.
Construction-time resolution failures are recorded, a rejected token is
not retried, and lookups run outside the scope lock with compare-and-bind.
Sub-agents reuse a parent scope only from a locked snapshot.
stop_impersonating! is a no-op unless impersonating. Client and Agent
refuse Marshal.dump, faraday: ConnectionOptions get the TLS and proxy
guards, loopback_host? parses addresses, and the unused LiveQuery
loopback constant is removed.
…whole

File URLs drop tabs and newlines, strip leading control characters,
check scheme-relative values against trusted_url_hosts, and treat
non-http(s) schemes as untrusted; trust_legacy_tfss_on_any_host can turn
off the tfss exception. Redaction runs on string values before encoding,
covers header-shaped text, and takes quoted values whole, so
Response#to_s and logged JSON stay valid and leak no quoted secrets.
The parser reads POSIX brackets like PCRE2, refuses \g<n> subroutine
calls, more than three wide quantifiers in a row, and spaced repeat
counts, and allows fixed-literal alternations and separator runs whose
branches cannot overlap even ignoring case. Regexp and BSON values under
$not/$in/$nin/$all are checked on REST. Mongo-direct reads carrying a
non-literal regex get a default max_time_ms of 5000 ms
(PipelineSecurity.default_regex_max_time_ms).
…laims

The test Parse Server gets PARSE_SERVER_WEBHOOK_KEY and the webhook
integration tests set Parse::Webhooks.key instead of allowing
unauthenticated ingress. claimed_master? accepts only a JSON true, and
signature verification and the authenticated flag come from one read of
the signing secret. Removes a stale LiveQuery note and unused constant.
@AdrianCurtin AdrianCurtin changed the title Release/5.8.2 v5.8.2 - Batch Credentials, Regex Checks, Agent and Webhook Trust Oct 8, 2026
Comment thread lib/parse/agent/mcp_server.rb Fixed
Comment thread lib/parse/query/constraints.rb Fixed
Comment thread lib/parse/query/constraints.rb Fixed
Comment thread test/lib/parse/regex_detector_582_test.rb Dismissed
Comment thread test/lib/parse/regex_detector_582_test.rb Dismissed
Comment thread test/lib/parse/regex_routing_582_test.rb Dismissed
normalize_api_key strips Unicode whitespace with a linear scan from each
end instead of an alternation that rescans every position, and the
regex parser reads {n,m} counts with a linear scanner instead of an
optional-comma pattern that backtracks on long digit runs. Clears
CodeQL rb/polynomial-redos alerts on both.
@AdrianCurtin
AdrianCurtin merged commit 1221f57 into main Oct 8, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants