parse-stack-next 5.8.1 → 5.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +312 -0
- data/README.md +6 -0
- data/docs/caching.md +40 -5
- data/docs/mcp_guide.md +7 -2
- data/docs/mongodb_direct_guide.md +29 -0
- data/docs/webhooks_guide.md +35 -2
- data/lib/parse/agent/constraint_translator.rb +22 -21
- data/lib/parse/agent/errors.rb +17 -0
- data/lib/parse/agent/mcp_server.rb +47 -6
- data/lib/parse/agent.rb +316 -50
- data/lib/parse/api/batch.rb +70 -2
- data/lib/parse/atlas_search.rb +1 -0
- data/lib/parse/client/batch.rb +231 -12
- data/lib/parse/client/body_builder.rb +100 -15
- data/lib/parse/client/caching.rb +53 -3
- data/lib/parse/client/request.rb +59 -0
- data/lib/parse/client/response.rb +63 -5
- data/lib/parse/client.rb +147 -31
- data/lib/parse/live_query/client.rb +70 -9
- data/lib/parse/model/classes/session.rb +44 -16
- data/lib/parse/model/core/actions.rb +22 -1
- data/lib/parse/model/file.rb +96 -19
- data/lib/parse/mongodb.rb +24 -1
- data/lib/parse/pipeline_security.rb +157 -21
- data/lib/parse/query/constraints.rb +634 -14
- data/lib/parse/query.rb +9 -5
- data/lib/parse/stack/tasks.rb +1 -1
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +6 -2
- data/lib/parse/vector_search/hybrid.rb +4 -1
- data/lib/parse/vector_search.rb +1 -0
- data/lib/parse/webhooks/payload.rb +39 -8
- data/lib/parse/webhooks/registration.rb +26 -6
- data/lib/parse/webhooks/replay_protection.rb +19 -9
- data/lib/parse/webhooks.rb +17 -4
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d49d6371c1e6873ea48401f789d9087e23870530ae405bf2629ea9db67360995
|
|
4
|
+
data.tar.gz: 9589dfb1ec150a78d527c6ad0097a3ee4263701b104baf7b54afb73e73ec3bdf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 22998b27d0f277d174759d29c5087675628eee3a9a24ed460c82c87deda6a28252ed1d1ad78daee9a2438b2a7b1c4f8995197450d9eb5922b5689e58bda52bdb
|
|
7
|
+
data.tar.gz: 5ef7ef2f07269ca95bbd985c806250ca7d93d02c7c6bccd83a9d72a0c9a57b990bac63e977ddfbd1c601481562fb12d5c0272639ec415787cef0cc76fc4dd3c0
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,317 @@
|
|
|
1
1
|
## parse-stack-next Changelog
|
|
2
2
|
|
|
3
|
+
### 5.8.2
|
|
4
|
+
|
|
5
|
+
A security and correctness patch. No new features beyond the `session:`
|
|
6
|
+
arguments and the cache opt-in the fixes need; intentional master-key and
|
|
7
|
+
analytics access is unchanged. Outcomes that change are listed under
|
|
8
|
+
Behavior Notes.
|
|
9
|
+
|
|
10
|
+
#### Transport and MCP authentication
|
|
11
|
+
|
|
12
|
+
- **FIXED**: A whitespace-only MCP API key (`api_key: " "`,
|
|
13
|
+
`MCP_API_KEY=" "`, or a non-breaking-space key) now counts as no key.
|
|
14
|
+
Before, it passed the check that refuses a non-loopback bind without a key
|
|
15
|
+
and then disabled request authentication, leaving a public MCP endpoint
|
|
16
|
+
open. The configured key and the provided `X-MCP-API-Key` header are
|
|
17
|
+
stripped the same way, an explicit blank `api_key:` no longer hides
|
|
18
|
+
`MCP_API_KEY`, and a non-loopback bind with a key shorter than 16
|
|
19
|
+
characters warns.
|
|
20
|
+
- **FIXED**: HTTPS and WebSocket scheme checks parse the URL and compare the
|
|
21
|
+
scheme case-insensitively. Before, `HTTP://` passed `require_https`,
|
|
22
|
+
`HTTPS://` with TLS verification turned off was not refused, and `WS://` got
|
|
23
|
+
past the plaintext LiveQuery guard.
|
|
24
|
+
- **FIXED**: The TLS guard also refuses `ssl: { verify_mode:
|
|
25
|
+
OpenSSL::SSL::VERIFY_NONE }`, `ssl: { verify_hostname: false }`, and a
|
|
26
|
+
`Faraday::SSLOptions` with verification off on an https server URL. Before,
|
|
27
|
+
only a Hash with `verify: false` was caught. A `faraday:` option passed as
|
|
28
|
+
`Faraday::ConnectionOptions` gets the same TLS and proxy guards as a Hash,
|
|
29
|
+
and any other non-Hash value raises.
|
|
30
|
+
- **FIXED**: `Parse::LiveQuery::Client.new` validates an explicit or
|
|
31
|
+
configured URL, as `Parse.setup` does. Plaintext `ws://` to a routable host
|
|
32
|
+
is refused without `allow_insecure`, and a URL whose scheme is not `ws`,
|
|
33
|
+
`wss`, `http`, or `https` raises `ArgumentError`. **CHANGED**: an
|
|
34
|
+
`https://` LiveQuery URL is used as `wss://` (it used to be opened without
|
|
35
|
+
TLS), and an `http://` URL as `ws://` under the same loopback rule, each
|
|
36
|
+
with a one-time deprecation warning.
|
|
37
|
+
- **FIXED**: `Parse::File` URLs hydrated from Parse JSON no longer skip the
|
|
38
|
+
`trusted_url_hosts` check when the scheme is uppercase (`HTTPS://`), the
|
|
39
|
+
URL has leading whitespace, or the URL is malformed (`https:host`,
|
|
40
|
+
`https:\\host`, `https:///host`), all of which browsers resolve to the
|
|
41
|
+
remote host. Under
|
|
42
|
+
`untrusted_url_policy = :raise` or `:strip`, a malformed http(s) URL is
|
|
43
|
+
treated as untrusted. `force_ssl` upgrades `HTTP://` URLs too. Hydration
|
|
44
|
+
reads a URL the way a browser does (tabs and newlines removed, leading
|
|
45
|
+
control characters stripped), checks scheme-relative values (`//host`,
|
|
46
|
+
`\\host`) against `trusted_url_hosts`, and treats `javascript:`, `data:`,
|
|
47
|
+
and other non-http(s) URLs as untrusted.
|
|
48
|
+
- **NEW**: `Parse::File.trust_legacy_tfss_on_any_host = false` requires
|
|
49
|
+
`tfss-` file URLs to come from `trusted_url_hosts`. The default (`true`)
|
|
50
|
+
keeps accepting them from any host.
|
|
51
|
+
- **FIXED**: `Parse::Response#inspect` prints only the status and the
|
|
52
|
+
result's shape, and `#to_s` replaces credential fields with `[FILTERED]`:
|
|
53
|
+
session tokens, passwords, `authData`, access and refresh tokens, API,
|
|
54
|
+
master, client, and JavaScript keys, MFA recovery codes and secrets
|
|
55
|
+
(`recovery`, `recoveryCodes`, `secret`, `authDataResponse`), and
|
|
56
|
+
storage-form columns (`_session_token`, `_hashed_password`,
|
|
57
|
+
`_email_verify_token`, `_perishable_token`, `_password_history`,
|
|
58
|
+
`_auth_data_*`). The same list drives log redaction, which also covers
|
|
59
|
+
header-shaped text (`X-Parse-Master-Key: ...`, `X-Parse-REST-API-Key`,
|
|
60
|
+
`X-Parse-Session-Token`, `X-Parse-Javascript-Key`, `X-Parse-Client-Key`,
|
|
61
|
+
`X-Parse-Webhook-Key`, `Authorization: Bearer ...`). `to_s` also filters
|
|
62
|
+
credential text inside string values (`password=...`), and its error form
|
|
63
|
+
redacts the request line and error text. Text patterns run on string values
|
|
64
|
+
before encoding, and a quoted value is redacted whole (spaces included,
|
|
65
|
+
and after an auth scheme: `password="a b"`, `Authorization: Bearer "x"`),
|
|
66
|
+
so the output stays valid JSON; log redaction of JSON bodies works the same
|
|
67
|
+
way. `#result` is unchanged.
|
|
68
|
+
- **IMPROVED**: Webhook endpoint registration accepts the scheme in any case
|
|
69
|
+
and warns when an `http://` endpoint on a routable host would carry the
|
|
70
|
+
webhook key in cleartext.
|
|
71
|
+
|
|
72
|
+
#### Batches and transactions
|
|
73
|
+
|
|
74
|
+
- **FIXED**: A batch keeps each write's credentials. Parse Server runs every
|
|
75
|
+
sub-request of a `POST /batch` under that call's own credentials and ignores
|
|
76
|
+
per-sub-request headers. Before, `Array#save`, `Array#destroy`,
|
|
77
|
+
`Parse.batch`, and `Parse::Object.transaction` sent everything through the
|
|
78
|
+
default client, so a write built for a user session (a class bound to
|
|
79
|
+
`Parse.client.become(token)`, or a `Parse::Request` with `session_token:` /
|
|
80
|
+
`use_master_key: false`) ran with the master key and bypassed ACLs and CLPs.
|
|
81
|
+
Writes are now sent through their own client and options, and a batch with
|
|
82
|
+
mixed credentials goes out as separate calls with responses in request
|
|
83
|
+
order. Credentials resolve exactly as for a single request: a request's
|
|
84
|
+
`session_token:` / `use_master_key:` options, its `X-Parse-Session-Token`
|
|
85
|
+
header, and its master-key suppression header (which keeps the master key
|
|
86
|
+
off even with `use_master_key: true`). The same rules apply to direct
|
|
87
|
+
`Parse::Client#batch_request` calls. A client chosen explicitly
|
|
88
|
+
(`BatchOperation#client=`, or the client `batch_request` is called on) is
|
|
89
|
+
used for every request, as `Parse::Client#request` uses it for a single
|
|
90
|
+
request; an object's class client applies only to implicit batches
|
|
91
|
+
(`Array#save`, `Array#destroy`, `Parse.batch`). A `use_master_key:` that is
|
|
92
|
+
not exactly `true` or `false` is treated as unset, a `session_token:` that
|
|
93
|
+
resolves to no token fails closed, and narrowing `batch_request` options
|
|
94
|
+
(`use_master_key: false`) override a request's own `use_master_key: true`.
|
|
95
|
+
- **FIXED**: `Parse::Object.transaction` runs with its objects' class client.
|
|
96
|
+
A transaction that mixes credentials raises
|
|
97
|
+
`Parse::BatchOperation::MixedAuthorityError` before anything is sent,
|
|
98
|
+
because Parse Server runs a transaction under one credential and it cannot
|
|
99
|
+
be split. Clients are compared by their credentials (server, application,
|
|
100
|
+
keys, and bound session), not by object identity, so classes whose clients
|
|
101
|
+
were configured identically, such as before and after a second
|
|
102
|
+
`Parse.setup`, still share one transaction. Requests that name their own
|
|
103
|
+
session token are compared without the master key or bound session, so
|
|
104
|
+
`transaction(session:)` across clients that differ only in those is sent as
|
|
105
|
+
one call.
|
|
106
|
+
- **FIXED**: `Array#destroy(session:)` on sessions looks up their tokens and
|
|
107
|
+
owners as that user, so deleting session ids the user cannot see no longer
|
|
108
|
+
evicts other users' cached identities or triggers a reset. A session the
|
|
109
|
+
caller could not read is still treated as deleted when its delete succeeds:
|
|
110
|
+
its recorded owner is invalidated, or the rate-limited reset applies. A
|
|
111
|
+
denied or not-found delete of such a row forgets nothing.
|
|
112
|
+
- **NEW**: `Array#save(session:)`, `Array#destroy(session:)`, and
|
|
113
|
+
`Parse::Object.transaction(session:)` send every write in the call as the
|
|
114
|
+
given user.
|
|
115
|
+
|
|
116
|
+
#### Agents
|
|
117
|
+
|
|
118
|
+
- **FIXED**: A session-token agent whose token could not be resolved when it
|
|
119
|
+
was built (for example, Parse Server unreachable) is no longer treated as
|
|
120
|
+
master-key posture by the SDK-side permission checks. The scope is resolved
|
|
121
|
+
again on first use; if it still cannot be, the call fails with
|
|
122
|
+
`Parse::Agent::UnresolvedIdentity` (reported as `:access_denied`,
|
|
123
|
+
`kind: :unresolved_identity`) instead of skipping the CLP check. Parse
|
|
124
|
+
Server already validated the token on REST calls.
|
|
125
|
+
- **FIXED**: `Parse::Agent#inspect`, `to_s`, and `pp` print a redacted summary
|
|
126
|
+
and no longer show the session token, the client's keys, the conversation,
|
|
127
|
+
or the last request and response.
|
|
128
|
+
- **FIXED**: `Parse::Agent#as_json`, `#to_json`, and `#to_yaml`, and the same
|
|
129
|
+
methods on `Parse::Client`, return a redacted summary, so JSON log
|
|
130
|
+
formatters and error trackers that serialize an agent or client no longer
|
|
131
|
+
emit its session token, master key, or REST key.
|
|
132
|
+
- **FIXED**: A sub-agent that holds its parent's session token on the
|
|
133
|
+
parent's client reuses the parent's resolved scope instead of resolving it again. A failed resolution
|
|
134
|
+
of a different child token is reported as `Parse::Agent::UnresolvedIdentity`,
|
|
135
|
+
not as an attempt to widen the parent's scope.
|
|
136
|
+
- **FIXED**: Lazy session resolution, `impersonate`, and `refresh_scope!` are
|
|
137
|
+
serialized per agent, so a concurrent `impersonate` cannot leave one
|
|
138
|
+
token's scope bound to another token. A failed lazy resolution is retried
|
|
139
|
+
after 5 seconds, not on every call. `refresh_scope!` resolves or raises for
|
|
140
|
+
a session agent instead of returning nil, and an invalid or expired token
|
|
141
|
+
gets its own error message. `acl_scope` checks and reads the scope under
|
|
142
|
+
the same lock, and `acl_permission_strings` never returns nil for a
|
|
143
|
+
session-token agent.
|
|
144
|
+
- **FIXED**: A session agent whose token cannot be resolved records the
|
|
145
|
+
failure at construction too, so the first use inside the 5-second window is
|
|
146
|
+
refused without a second lookup, and a token Parse Server rejected is not
|
|
147
|
+
retried. The lookup runs outside the agent's scope lock, so other threads
|
|
148
|
+
using the agent are not blocked for a request timeout, and the result binds
|
|
149
|
+
only if the token is unchanged. A sub-agent reuses its parent's scope only
|
|
150
|
+
from a token-and-scope snapshot taken under the parent's lock.
|
|
151
|
+
- **FIXED**: `Parse::Client` and `Parse::Agent` refuse `Marshal.dump`
|
|
152
|
+
(`TypeError`), so a client's master key or an agent's session token cannot
|
|
153
|
+
land in a marshaled cache or job payload.
|
|
154
|
+
- **CHANGED**: `Parse::Agent#stop_impersonating!` is a no-op on an agent that
|
|
155
|
+
is not impersonating, instead of dropping a constructor-supplied session
|
|
156
|
+
token and leaving master-key posture.
|
|
157
|
+
|
|
158
|
+
#### Response cache
|
|
159
|
+
|
|
160
|
+
- **FIXED**: Reads made with a session token are no longer served from the
|
|
161
|
+
response cache. A cached answer was returned without contacting Parse
|
|
162
|
+
Server, so a revoked session (logout, `Parse::Session#destroy`,
|
|
163
|
+
`logout_all!`, a password change), or a user who lost a role or row access,
|
|
164
|
+
kept reading the cached rows until the entry expired. Session reads now
|
|
165
|
+
always reach Parse Server, whether the session came from `session_token:`,
|
|
166
|
+
`Parse.with_session`, or a session-bound client. Master-key and anonymous
|
|
167
|
+
reads are cached as before, and writes made with a session still invalidate
|
|
168
|
+
cached entries.
|
|
169
|
+
- **NEW**: `Parse::Middleware::Caching.cache_session_requests = true` (or
|
|
170
|
+
per client, `Parse.setup(cache_session_requests: true)`, which overrides the
|
|
171
|
+
class default either way) opts back into caching session reads, for apps
|
|
172
|
+
that accept revocation being bounded by the cache TTL. The
|
|
173
|
+
`parse.cache.bypass` event includes `cache_tenant`.
|
|
174
|
+
|
|
175
|
+
#### Regex validation and direct reads
|
|
176
|
+
|
|
177
|
+
- **FIXED**: A raw `$regex` in a where hash
|
|
178
|
+
(`Model.query(name: { "$regex" => ... })`, `:field.eq`, `:field.not`,
|
|
179
|
+
`$elemMatch`, and `:or` branches) goes through the same ReDoS check as
|
|
180
|
+
`:field.like`. Before, only the regex constraints themselves were checked.
|
|
181
|
+
Ruby `Regexp` and BSON regex values under `$not`, `$in`, `$nin`, and `$all`
|
|
182
|
+
are checked too.
|
|
183
|
+
- **FIXED**: Mongo-direct filters and pipelines (`results_direct`,
|
|
184
|
+
`count_direct`, `Query#aggregate`, `Parse::MongoDB.aggregate` and `find`,
|
|
185
|
+
Atlas Search and vector filters, LiveQuery `where`, and the agent
|
|
186
|
+
`aggregate` tool) run every `$regex`, Ruby `Regexp`, BSON regex, and
|
|
187
|
+
`$regexMatch` / `$regexFind` / `$regexFindAll` pattern through the ReDoS
|
|
188
|
+
check. Before, only the pattern length was capped. The agent constraint
|
|
189
|
+
translator applies the same check.
|
|
190
|
+
- **FIXED**: `Parse::RegexSecurity` parses each caller-supplied pattern
|
|
191
|
+
instead of matching a few substrings. It refuses a group repeated more than
|
|
192
|
+
once whose body holds a variable-count quantifier (including an optional
|
|
193
|
+
`?`), an alternation, or a backreference (`(a+)+`, `(a|aa)+$`, `((a+))+`,
|
|
194
|
+
`(a+){2,}`, `(a+){20}`, `^(a?){100}a{100}$`), inline comments
|
|
195
|
+
`(?#...)`, extended mode, and patterns it cannot read. Before, several of
|
|
196
|
+
these shapes passed. Repeats of a single atom (`^.{1,255}$`, `\d{1,100}`)
|
|
197
|
+
and simple lookarounds (`^(?!test)`) are accepted; the old checker refused
|
|
198
|
+
them for `:field.like`. Escaped literal patterns
|
|
199
|
+
built by `contains`, `starts_with`, and `ends_with` get a longer length cap,
|
|
200
|
+
so long values keep compiling. It also refuses subroutine calls (`\g<1>`,
|
|
201
|
+
`\g'1'`), more than three unbounded or wide quantifiers in a row with no
|
|
202
|
+
separating literal they cannot match (`\d+\d+\d+\d+`, `.*a.*a.*a.*a`),
|
|
203
|
+
and whitespace inside a repeat count (`{ 2,}`), and reads `[:` that is not a
|
|
204
|
+
complete POSIX class as a literal, as PCRE2 does. A repeated group is
|
|
205
|
+
allowed when each repeat cannot split its input two ways: fixed
|
|
206
|
+
alternatives whose first characters differ even ignoring case
|
|
207
|
+
(`^(foo|bar)+$`, but not `(?i)^(a|Aa)+$`) or a literal
|
|
208
|
+
separator next to one run that cannot match it
|
|
209
|
+
(`^[a-z0-9]+(?:-[a-z0-9]+)*$`, `(\.[\w-]+)+`, `([\w-]+\.)+`).
|
|
210
|
+
- **FIXED**: `$regexMatch`, `$regexFind`, and `$regexFindAll` in
|
|
211
|
+
caller-supplied pipelines require a literal `regex` (a String that is not a
|
|
212
|
+
field path or variable, a Regexp, or a BSON regex). An expression or
|
|
213
|
+
`"$field"` operand raises `Parse::PipelineSecurity::Error`
|
|
214
|
+
(`reason: :regex_not_literal`).
|
|
215
|
+
- **IMPROVED**: Mongo-direct reads (`Parse::MongoDB.aggregate` and `find`,
|
|
216
|
+
`results_direct`, `count_direct`, Atlas Search, and vector and hybrid
|
|
217
|
+
search) whose filter or pipeline carries a regex that is not escaped literal
|
|
218
|
+
text get a server-side `max_time_ms` of
|
|
219
|
+
`Parse::PipelineSecurity.default_regex_max_time_ms` (5000 ms) when the
|
|
220
|
+
caller passed none. Set it to nil to turn this off; an explicit
|
|
221
|
+
`max_time_ms:` always wins. For REST queries, set Parse Server's
|
|
222
|
+
`databaseOptions.maxTimeMS`.
|
|
223
|
+
- **FIXED**: `Parse::MongoDB.find` raises `Parse::ACLScope::ACLRequired` inside
|
|
224
|
+
`Parse.without_master_key` instead of reading raw documents unscoped.
|
|
225
|
+
`Parse::MongoDB.indexes` (metadata only) stays available.
|
|
226
|
+
|
|
227
|
+
#### Webhooks
|
|
228
|
+
|
|
229
|
+
- **FIXED**: A webhook request's `master` flag is trusted only when the
|
|
230
|
+
request is authenticated: the webhook key matched, or a configured
|
|
231
|
+
signature verified. Under `Parse::Webhooks.allow_unauthenticated` with no
|
|
232
|
+
signature, a body claiming `"master": true` no longer passes master-only
|
|
233
|
+
field guards, ACL owner resolution, or handler `payload.master?` checks, and
|
|
234
|
+
no longer triggers the Ruby-initiated callback dedup, so a forged `_RB_`
|
|
235
|
+
request id cannot make a write skip model callbacks (including a
|
|
236
|
+
`before_save` that rejects it). Only a JSON `true` counts as master.
|
|
237
|
+
`payload.authenticated?` reports whether the request was authenticated, and
|
|
238
|
+
`payload.claimed_master?` exposes the raw claim; both are for diagnostics
|
|
239
|
+
only. Signature verification and the authenticated flag come from one read
|
|
240
|
+
of the signing secret.
|
|
241
|
+
|
|
242
|
+
#### Behavior Notes
|
|
243
|
+
|
|
244
|
+
- **Transport.** A LiveQuery `https://` URL is used as `wss://` (and a
|
|
245
|
+
loopback `http://` as `ws://`) with a deprecation warning; configure the
|
|
246
|
+
`ws(s)://` URL directly. Other LiveQuery schemes raise. `HTTP://` with
|
|
247
|
+
`require_https`, and an https server URL with TLS verification disabled
|
|
248
|
+
(`verify: false`, `verify_mode`, `verify_hostname`, or `SSLOptions`), raise.
|
|
249
|
+
Loopback hosts for these checks are `localhost`, `127.0.0.0/8`, `::1`, and
|
|
250
|
+
`0.0.0.0`, parsed as addresses (`127.999.1.1` and `localhost.` are not
|
|
251
|
+
loopback). A padded MCP API key is enforced without its surrounding
|
|
252
|
+
whitespace. Hydrated `Parse::File` URLs with an uppercase scheme or leading
|
|
253
|
+
whitespace are checked against `trusted_url_hosts`; malformed http(s) URLs
|
|
254
|
+
are refused or stripped under `:raise` / `:strip` and warn under `:warn`,
|
|
255
|
+
as are non-http(s) schemes (`javascript:`, `data:`, `file:`) and
|
|
256
|
+
scheme-relative values pointing at an untrusted host.
|
|
257
|
+
- **Printing.** `Parse::Response#to_s` replaces credential-bearing values with
|
|
258
|
+
`"[FILTERED]"` and `#inspect` no longer shows values; read `#result` (or
|
|
259
|
+
`#result.to_json`) for raw data. `to_s` may also filter text in string
|
|
260
|
+
values that looks like a credential (`password=...`). `Parse::Agent` and `Parse::Client`
|
|
261
|
+
`inspect`, `to_s`, `as_json`, `to_json`, and `to_yaml` print redacted
|
|
262
|
+
summaries.
|
|
263
|
+
- **Agents.** A session-token agent that cannot resolve its token refuses
|
|
264
|
+
tool calls that need its permissions. `Agent#acl_scope` can now raise
|
|
265
|
+
`Parse::Agent::UnresolvedIdentity`, and `acl_scope?` is true for any
|
|
266
|
+
session-token agent; custom handlers that test `agent.acl_scope` for
|
|
267
|
+
truthiness should use `agent.acl_scope?`. A sub-agent of an unresolved
|
|
268
|
+
parent can inherit the parent's token, but one with a different identity
|
|
269
|
+
raises until the parent's token resolves. A session agent built while Parse
|
|
270
|
+
Server is unreachable refuses calls for 5 seconds before retrying.
|
|
271
|
+
Marshaling a `Parse::Client` or `Parse::Agent` raises `TypeError`; store the
|
|
272
|
+
configuration and build a new client instead.
|
|
273
|
+
- **Response cache.** Queries that set `cache: true` (or run under
|
|
274
|
+
`Parse.default_query_cache = true`) with a session token no longer get cache
|
|
275
|
+
hits. Set `cache_session_requests` to `true` (only a literal `true`
|
|
276
|
+
enables it) to restore the old behavior, and keep `expires:` short if you
|
|
277
|
+
do.
|
|
278
|
+
- **Regex.** A raw `$regex`, or a `Regexp` in a mongo-direct filter, that the
|
|
279
|
+
ReDoS check rejects now raises: `ArgumentError` on REST queries,
|
|
280
|
+
`Parse::PipelineSecurity::Error` (or `Parse::MongoDB::DeniedOperator`) on
|
|
281
|
+
direct paths. Escaped literal patterns, including those built by
|
|
282
|
+
`starts_with`, `ends_with`, and `contains`, pass the shape check and get a
|
|
283
|
+
length cap about twice the normal one. Caller-supplied `$options` accept
|
|
284
|
+
`i`, `m`, `s`, and `u`; an unknown flag, a non-String value, the extended
|
|
285
|
+
flag `x`, or a Regexp built with `Regexp::EXTENDED` raises. The agent
|
|
286
|
+
translator now accepts `s` and `u` and no longer accepts `x`. A pattern with
|
|
287
|
+
more than three unbounded quantifiers in a row and no literal between them
|
|
288
|
+
(for example `^\w+\s+\w+\s+\w+\s+\w+$`) is refused; add a literal
|
|
289
|
+
separator or split the query. A mongo-direct read carrying a non-literal
|
|
290
|
+
regex and no `max_time_ms` times out after 5 seconds with
|
|
291
|
+
`Parse::MongoDB::ExecutionTimeout`; pass `max_time_ms:` or set
|
|
292
|
+
`Parse::PipelineSecurity.default_regex_max_time_ms` to change it.
|
|
293
|
+
- **Direct reads.** Code that calls `Parse::MongoDB.find` inside
|
|
294
|
+
`Parse.without_master_key` should wrap it in `Parse.with_master_key` or use
|
|
295
|
+
a scoped read. `Parse::MongoDB.collection` returns raw driver access with no
|
|
296
|
+
scope and is not refused inside the block.
|
|
297
|
+
- **Batches.** A batch or transaction of objects whose class is bound to a
|
|
298
|
+
session client now runs as that user, as a single save does. A transaction
|
|
299
|
+
mixing credentials raises `MixedAuthorityError`, and a non-transactional
|
|
300
|
+
batch with mixed credentials is sent as several calls. An explicitly chosen
|
|
301
|
+
batch client (`client=` or the `batch_request` receiver) is used for every
|
|
302
|
+
request instead of each object's class client.
|
|
303
|
+
`Client#batch_request` accepts `session_token:` and `use_master_key:`. A
|
|
304
|
+
blank `session:`, or a user with no session token, raises `ArgumentError`.
|
|
305
|
+
- **Webhooks.** `payload.master?` and `payload.claimed_master?` no longer
|
|
306
|
+
treat the string `"true"` as master; Parse Server always sends a JSON
|
|
307
|
+
boolean. Under `Parse::Webhooks.allow_unauthenticated` with no signing
|
|
308
|
+
secret, `payload.master?` is false even for a master-key request Parse
|
|
309
|
+
Server forwards, so master-only field guards and handler `master?` checks
|
|
310
|
+
refuse it, and saves made by the SDK itself run their model callbacks in
|
|
311
|
+
process and again through the webhook. Configure a webhook key
|
|
312
|
+
(`PARSE_SERVER_WEBHOOK_KEY` on Parse Server and `Parse::Webhooks.key` in the
|
|
313
|
+
app) or a signing secret to restore master trust and dedup.
|
|
314
|
+
|
|
3
315
|
### 5.8.1
|
|
4
316
|
|
|
5
317
|
A correctness release for webhooks, sessions, associations, queries, search,
|
data/README.md
CHANGED
|
@@ -3069,8 +3069,13 @@ songs.save
|
|
|
3069
3069
|
|
|
3070
3070
|
# you can also destroy a set of objects
|
|
3071
3071
|
songs.destroy
|
|
3072
|
+
|
|
3073
|
+
# send every write as a specific user (ACL and CLP enforced)
|
|
3074
|
+
songs.save(session: user)
|
|
3072
3075
|
```
|
|
3073
3076
|
|
|
3077
|
+
Each write in a batch keeps the credentials it was built for. Parse Server runs a `POST /batch` under one credential, so objects whose classes use clients with different credentials (for example a class bound to `Parse.client.become(token)`; identically configured clients are merged), or raw `Parse::Request`s carrying different `session_token:` or `use_master_key:` options, are sent as separate batch calls, each with its own credentials. Responses still come back in request order.
|
|
3078
|
+
|
|
3074
3079
|
### Magic `save_all`
|
|
3075
3080
|
By default, all Parse queries have a maximum fetch limit of 1000. While using the `:max` option, Parse-Stack can increase this up to 11,000. In the cases where you need to update a large number of objects, you can utilize the `Parse::Object#save_all` method to fetch, modify and save objects.
|
|
3076
3081
|
|
|
@@ -3131,6 +3136,7 @@ end
|
|
|
3131
3136
|
- **Mixed operations**: Support create, update, and delete operations in single transaction
|
|
3132
3137
|
- **Error handling**: Comprehensive error handling with meaningful exception messages
|
|
3133
3138
|
- **Object ID assignment**: New objects automatically receive their `objectId`, `createdAt`, and `updatedAt` from the server response after successful transaction
|
|
3139
|
+
- **One credential**: A transaction runs with the credentials of its objects' class client, or as a given user with `Parse::Object.transaction(session: user)`. Objects bound to clients with different credentials raise `Parse::BatchOperation::MixedAuthorityError` before anything is sent, because a transaction cannot be split.
|
|
3134
3140
|
|
|
3135
3141
|
```ruby
|
|
3136
3142
|
# Transaction with custom retry limit and error handling
|
data/docs/caching.md
CHANGED
|
@@ -230,6 +230,38 @@ caching path the two never meet. Three points are worth knowing anyway.
|
|
|
230
230
|
|
|
231
231
|
## Auth scoping of cached responses
|
|
232
232
|
|
|
233
|
+
### Session reads are not cached by default
|
|
234
|
+
|
|
235
|
+
A read made with a session token is never read from or stored in the response
|
|
236
|
+
cache unless the application opts in:
|
|
237
|
+
|
|
238
|
+
```ruby
|
|
239
|
+
Parse::Middleware::Caching.cache_session_requests = true # every client
|
|
240
|
+
Parse.setup(cache_session_requests: true, ...) # one client
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Only `true` enables it (a `"true"` String from an environment variable does
|
|
244
|
+
not). A client's own `cache_session_requests:` option overrides the class
|
|
245
|
+
default in either direction.
|
|
246
|
+
|
|
247
|
+
A cached answer is served without contacting Parse Server, so it cannot notice
|
|
248
|
+
that the session was revoked (logout, `Parse::Session#destroy`, `logout_all!`,
|
|
249
|
+
a password change) or that the user lost a role or row access through a change
|
|
250
|
+
this process did not make. With the default, every REST session read reaches
|
|
251
|
+
Parse Server, which checks the token and the current ACLs and CLPs each time.
|
|
252
|
+
Mongo-direct reads, including agent tools that route there, do not go through
|
|
253
|
+
this cache; they resolve the session through the identity plane, whose
|
|
254
|
+
revocation behavior is described in
|
|
255
|
+
[Identity and role caching](#identity-and-role-caching). This
|
|
256
|
+
applies however the session reached the request: an explicit `session_token:`,
|
|
257
|
+
`Parse.with_session`, or a client bound to a session. Master-key and
|
|
258
|
+
anonymous reads cache as before, and writes made with a session still
|
|
259
|
+
invalidate cached entries. Turning the option on trades that guarantee for
|
|
260
|
+
speed: a revoked or narrowed session keeps reading its cached responses until
|
|
261
|
+
they expire, so keep `expires:` short if you enable it.
|
|
262
|
+
|
|
263
|
+
### The auth discriminator
|
|
264
|
+
|
|
233
265
|
The cache key carries an auth discriminator, and this is a correctness property
|
|
234
266
|
rather than an optimization.
|
|
235
267
|
|
|
@@ -251,9 +283,9 @@ URL digest is placed before the discriminator, so every auth variant of one
|
|
|
251
283
|
resource shares a prefix, which is exactly what lets a write invalidate the
|
|
252
284
|
resource for all callers with a single pattern delete.
|
|
253
285
|
|
|
254
|
-
One consequence worth planning for
|
|
255
|
-
cache entry per session per URL. Cardinality
|
|
256
|
-
with distinct resources.
|
|
286
|
+
One consequence worth planning for, once session caching is enabled: a heavily
|
|
287
|
+
multi-user endpoint produces one cache entry per session per URL. Cardinality
|
|
288
|
+
scales with active sessions, not with distinct resources.
|
|
257
289
|
|
|
258
290
|
## Identity and role caching
|
|
259
291
|
|
|
@@ -703,8 +735,10 @@ Work down this list.
|
|
|
703
735
|
so deliberately.
|
|
704
736
|
5. Is this a `fetch!` or `reload!`? Those default to write-only mode, which by
|
|
705
737
|
design never reads from the cache. Use `fetch_cache!` to accept a cached body.
|
|
706
|
-
6. Is the
|
|
707
|
-
|
|
738
|
+
6. Is the read made with a session token? Session reads bypass the cache
|
|
739
|
+
unless `Parse::Middleware::Caching.cache_session_requests = true`. With it
|
|
740
|
+
on, entries are not shared across auth identities, so the first request for
|
|
741
|
+
each session is always a miss.
|
|
708
742
|
7. Is the store process-local? A Moneta memory store behind several workers hits
|
|
709
743
|
only when the same worker handles the repeat.
|
|
710
744
|
8. Has something set `Parse::Middleware::Caching.enabled = false`? That is the
|
|
@@ -725,6 +759,7 @@ All events are `ActiveSupport::Notifications`.
|
|
|
725
759
|
| `parse.cache.store` | the same, plus `duration_ms` |
|
|
726
760
|
| `parse.cache.delete` | the same, emitted on an invalidating write |
|
|
727
761
|
| `parse.cache.error` | the same, plus `error` (the exception class name only) |
|
|
762
|
+
| `parse.cache.bypass` | the same, plus `reason: :session` for a session read kept out of the cache |
|
|
728
763
|
| `parse.cache.evict` | `pattern_digest`, `deleted`, `duration_ms` |
|
|
729
764
|
| `parse.synchronize_create.acquired` | `key_digest`, `wait_ms` |
|
|
730
765
|
| `parse.synchronize_create.contended` | `key_digest`, `elapsed_ms` |
|
data/docs/mcp_guide.md
CHANGED
|
@@ -1672,7 +1672,12 @@ class Project < Parse::Object
|
|
|
1672
1672
|
# Forward the agent's scope to any internal query — pre-filtering by
|
|
1673
1673
|
# _wperm so the update only sees rows the agent's scope is allowed
|
|
1674
1674
|
# to modify, defense-in-depth alongside Parse Server's own ACL.
|
|
1675
|
-
|
|
1675
|
+
# acl_scope? is true for any scoped agent, including a session agent whose
|
|
1676
|
+
# token has not resolved yet (acl_scope itself would raise
|
|
1677
|
+
# Parse::Agent::UnresolvedIdentity for that one); acl_scope_kwargs
|
|
1678
|
+
# forwards the identity, and the scoped read fails closed if it cannot
|
|
1679
|
+
# be resolved.
|
|
1680
|
+
Audit.all(**agent.acl_scope_kwargs).each { |a| a.cancel! } if agent&.acl_scope?
|
|
1676
1681
|
self.archived = true # property :archived, :boolean
|
|
1677
1682
|
self.archive_reason = reason # property :archive_reason, :string
|
|
1678
1683
|
save
|
|
@@ -3439,7 +3444,7 @@ Parse::Agent.rack_app(logger: Rails.logger) { |env| ... }
|
|
|
3439
3444
|
|
|
3440
3445
|
**Sub-agent auth-scope inheritance and permissions clamp (v4.2).** When a tool handler constructs a sub-agent with `Parse::Agent.new(parent: agent, ...)`, the sub inherits `session_token` and `tenant_id` from the parent unless explicitly overridden. Without this inheritance, a session-token parent would silently produce a master-key sub-agent — the constructor default `session_token: nil` resolves to master-key mode — escalating privilege through the very kwarg meant to close sub-agent footguns. Explicit overrides still work (`Parse::Agent.new(parent: agent, session_token: nil)` produces a master-key sub if that is genuinely what the handler wants), but the default is fail-safe inheritance. `permissions:` is NOT inherited and defaults to `:readonly`, but the constructor enforces a clamp: an explicit `permissions:` override on a sub-agent is accepted only if `≤ parent.permissions`, otherwise `ArgumentError` is raised at construction. The clamp is the structural guarantee that a delegation chain cannot escape the parent's tier through sub-agent construction. See [Per-Agent Tool Filtering & Sub-Agent Delegation](#per-agent-tool-filtering--sub-agent-delegation-v42) for the full inheritance table.
|
|
3441
3446
|
|
|
3442
|
-
**Agent-level ACL scope: `session_token:` / `acl_user:` / `acl_role:` (v4.4.0).** `Parse::Agent.new` accepts three mutually-exclusive identity inputs. `session_token:` round-trips Parse Server's `/users/me` at construction
|
|
3447
|
+
**Agent-level ACL scope: `session_token:` / `acl_user:` / `acl_role:` (v4.4.0).** `Parse::Agent.new` accepts three mutually-exclusive identity inputs. `session_token:` round-trips Parse Server's `/users/me` at construction. If the server is unreachable then, the agent retries on the first tool call that needs its permissions; if that also fails, the call raises `Parse::Agent::UnresolvedIdentity` (reported as `:access_denied`, `kind: :unresolved_identity`) instead of running without the SDK-side check, and further calls within the next few seconds are refused without another lookup (5.8.2). `acl_user:` takes a `Parse::User` or User-pointer and expands the user's role subscription via `Parse::Role.all_for_user`. It makes no token round-trip, and the SDK enforces the resulting `_rperm` filter itself. `acl_role:` is service-account-style scoping: no user_id, just the role plus parent-role inheritance. Master-key posture (none of the three supplied) remains the default and still emits the one-time `[Parse::Agent:SECURITY]` banner at construction. Every built-in tool reads `agent.acl_scope_kwargs` (single point of truth) to forward identity into `Parse::MongoDB.aggregate`, `Parse::Query#results_direct`, and `Parse::AtlasSearch.{search,autocomplete}`. Developer-registered tool handlers and `agent_method` bodies can reach `agent.acl_scope`, `agent.acl_permission_strings`, `agent.acl_read_match_stage` (a `_rperm` `$match`), or `agent.acl_write_match_stage` (a `_wperm` `$match`) to apply the agent's identity to their own queries.
|
|
3443
3448
|
|
|
3444
3449
|
**ACL composition on the mongo-direct aggregate path (v4.4.0).** When `aggregate` routes through `Parse::MongoDB.aggregate` (the default when `Parse::MongoDB.enabled?` is true), the agent layer derives the auth posture from the agent instance and forwards it to ACLScope — session-tokened / acl_user / acl_role agents get the same row-level `_rperm` `$match` injection regardless of identity mode; master-key agents pass `master: true` (the agent's class/field/tenant/canonical-filter gates are the security boundary for that posture). The posture is built in `Parse::Agent#acl_scope_kwargs`, not from tool-call JSON arguments; LLM-supplied `master:`, `session_token:`, `acl_user:`, or `acl_role:` kwargs are silently swallowed by the tool signature's `**_kwargs` catchall and never reach `Parse::MongoDB.aggregate`. An LLM cannot escalate from a scoped posture to master-key by injecting `master: true` into the tool arguments.
|
|
3445
3450
|
|
|
@@ -287,6 +287,9 @@ raw = Parse::MongoDB.find(
|
|
|
287
287
|
Convenience wrapper around `db.find`. Accepts `limit:`, `skip:`, `sort:`,
|
|
288
288
|
`projection:`, `hint:`, `max_time_ms:`. When `:limit` is omitted the call applies
|
|
289
289
|
`DEFAULT_FIND_LIMIT = 1000` and warns; pass `limit: 0` to opt out.
|
|
290
|
+
`find` reads raw documents unscoped, so it raises `Parse::ACLScope::ACLRequired`
|
|
291
|
+
inside `Parse.without_master_key`; `Parse::MongoDB.indexes` (metadata only) is
|
|
292
|
+
allowed there.
|
|
290
293
|
|
|
291
294
|
### Forcing an index with `hint`
|
|
292
295
|
|
|
@@ -761,6 +764,32 @@ legitimate read stages — but they read from arbitrary collections. Never
|
|
|
761
764
|
pass attacker-controlled input into a pipeline; build the pipeline in
|
|
762
765
|
trusted code and interpolate only validated values.
|
|
763
766
|
|
|
767
|
+
#### Regex patterns and the default time limit
|
|
768
|
+
|
|
769
|
+
Every caller-supplied regex on these paths (`$regex` strings, Ruby
|
|
770
|
+
`Regexp` and BSON regex values, and the `regex` operand of `$regexMatch`,
|
|
771
|
+
`$regexFind`, and `$regexFindAll`) goes through `Parse::RegexSecurity`,
|
|
772
|
+
which parses the pattern and refuses shapes that backtrack heavily on
|
|
773
|
+
PCRE: nested or overlapping repeats such as `(a+)+` or `(a|aa)+`, more
|
|
774
|
+
than three unbounded quantifiers in a row with no separating literal
|
|
775
|
+
(`\d+\d+\d+\d+`), subroutine calls (`\g<1>`), inline comments, and
|
|
776
|
+
extended mode. Escaped literal text, such as the patterns `starts_with`,
|
|
777
|
+
`ends_with`, and `contains` build, always passes the shape check.
|
|
778
|
+
|
|
779
|
+
A pattern check cannot prove every regex is cheap, so mongo-direct reads
|
|
780
|
+
whose filter or pipeline carries a regex that is not escaped literal text
|
|
781
|
+
also get a server-side `max_time_ms` of
|
|
782
|
+
`Parse::PipelineSecurity.default_regex_max_time_ms` (5000 ms by default)
|
|
783
|
+
when the caller passed none. This covers `Parse::MongoDB.aggregate` and
|
|
784
|
+
`find`, `results_direct`, `count_direct`, Atlas Search, and vector and
|
|
785
|
+
hybrid search. Set the attribute to a different value, or to `nil` to
|
|
786
|
+
turn the default off; an explicit `max_time_ms:` always wins. A query that
|
|
787
|
+
runs past the limit raises `Parse::MongoDB::ExecutionTimeout`.
|
|
788
|
+
|
|
789
|
+
REST queries are executed by Parse Server, which applies no regex check of
|
|
790
|
+
its own. Set `databaseOptions.maxTimeMS` in the Parse Server configuration
|
|
791
|
+
to bound those.
|
|
792
|
+
|
|
764
793
|
### Layer 2: Row-level ACL enforcement (`Parse::ACLScope`) — scoped only
|
|
765
794
|
|
|
766
795
|
When `Parse::MongoDB.aggregate` is called with `session_token:`,
|
data/docs/webhooks_guide.md
CHANGED
|
@@ -210,6 +210,36 @@ run Parse::Webhooks
|
|
|
210
210
|
See [`examples/webhook_server.rb`](../examples/webhook_server.rb) for a complete,
|
|
211
211
|
runnable setup.
|
|
212
212
|
|
|
213
|
+
### Authenticating requests, and what `master?` means
|
|
214
|
+
|
|
215
|
+
Requests are accepted only when `X-Parse-Webhook-Key` matches
|
|
216
|
+
`Parse::Webhooks.key` (`PARSE_SERVER_WEBHOOK_KEY`). With no key configured the
|
|
217
|
+
Rack app refuses every request unless you opt in with
|
|
218
|
+
`Parse::Webhooks.allow_unauthenticated = true` (or
|
|
219
|
+
`PARSE_WEBHOOK_ALLOW_UNAUTHENTICATED=true`), which is meant for local
|
|
220
|
+
development and tests. A signing secret does not replace the key: with no key
|
|
221
|
+
configured, a signed request is still refused unless `allow_unauthenticated`
|
|
222
|
+
is on. When both are configured, the key must match and the signature must
|
|
223
|
+
verify.
|
|
224
|
+
|
|
225
|
+
`payload.master?` is true only when the body says the master key was used (a
|
|
226
|
+
JSON `true`; Parse Server never sends a string) AND
|
|
227
|
+
the request was authenticated: the webhook key matched, or a configured
|
|
228
|
+
signature (`PARSE_WEBHOOK_SIGNING_SECRET`) verified. On unauthenticated ingress
|
|
229
|
+
any caller can put `"master": true` in the body, so `master?` returns false
|
|
230
|
+
there. Master-only field guards, ACL owner resolution, and handler checks
|
|
231
|
+
built on `master?` therefore treat such a request as a normal client request.
|
|
232
|
+
The Ruby-initiated callback dedup uses `master?` too, so a forged `_RB_`
|
|
233
|
+
request id plus `"master": true` cannot make a request skip model callbacks
|
|
234
|
+
(including a `before_save` that would reject it). `payload.claimed_master?`
|
|
235
|
+
returns the body's raw claim and `payload.authenticated?` reports whether the
|
|
236
|
+
request was authenticated; both are for diagnostics only.
|
|
237
|
+
|
|
238
|
+
The trade-off: on unauthenticated ingress, a save made by the SDK itself is
|
|
239
|
+
not recognized as Ruby-initiated, so its model callbacks can run twice (once
|
|
240
|
+
in process, once through the webhook). Configure a webhook key or a signing
|
|
241
|
+
secret to keep the dedup.
|
|
242
|
+
|
|
213
243
|
## Auditing trigger coverage
|
|
214
244
|
|
|
215
245
|
The wiring above has three independent moving parts, and a callback runs
|
|
@@ -486,8 +516,11 @@ master key). It has already run the model's `before_save` / `after_save` /
|
|
|
486
516
|
The intent is to keep trigger logic local when possible and run it exactly once.
|
|
487
517
|
Note that any logic in the **webhook block itself** still runs; only the
|
|
488
518
|
duplicate ActiveModel callback pass is skipped. A spoofed `_RB_` marker without
|
|
489
|
-
the master key does not get this treatment
|
|
490
|
-
|
|
519
|
+
the master key does not get this treatment, and neither does a claimed master
|
|
520
|
+
key on an unauthenticated request (no webhook key match and no verified
|
|
521
|
+
signature): the callbacks run in the webhook as usual. On unauthenticated
|
|
522
|
+
ingress this means SDK-initiated saves run their callbacks twice; configure a
|
|
523
|
+
webhook key or signing secret to keep the dedup.
|
|
491
524
|
|
|
492
525
|
### 2. Server-initiated replay / freshness protection (inbound)
|
|
493
526
|
|
|
@@ -117,17 +117,16 @@ module Parse
|
|
|
117
117
|
# worst-case backtracking cost on any one pattern bounded.
|
|
118
118
|
MAX_REGEX_PATTERN_LENGTH = 256
|
|
119
119
|
|
|
120
|
-
# Allowed $options flag characters
|
|
121
|
-
#
|
|
122
|
-
#
|
|
123
|
-
#
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
#
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
#
|
|
130
|
-
# backtracking). Matches a quantifier (`+` or `*`) INSIDE a
|
|
120
|
+
# Allowed $options flag characters: the shared
|
|
121
|
+
# {Parse::RegexSecurity::ALLOWED_OPTIONS} list (`imsu`). The extended
|
|
122
|
+
# flag `x` is refused because it lets comments hide a quantifier from
|
|
123
|
+
# the pattern check.
|
|
124
|
+
ALLOWED_REGEX_OPTIONS = defined?(Parse::RegexSecurity::ALLOWED_OPTIONS) ? Parse::RegexSecurity::ALLOWED_OPTIONS : "imsu"
|
|
125
|
+
|
|
126
|
+
# Older heuristic for nested-quantifier ReDoS patterns, kept for
|
|
127
|
+
# callers that reference it. Patterns are now checked by
|
|
128
|
+
# {Parse::RegexSecurity.validate!}, which parses the pattern. This
|
|
129
|
+
# heuristic matches a quantifier (`+` or `*`) INSIDE a
|
|
131
130
|
# parenthesized group that is itself followed by a quantifier
|
|
132
131
|
# (`+`, `*`, or `?`) — the structural shape that drives
|
|
133
132
|
# exponential time on adversarial inputs (`(a+)+`, `(a*)*`,
|
|
@@ -435,15 +434,17 @@ module Parse
|
|
|
435
434
|
#
|
|
436
435
|
# 1. $regex must be a String. No Hash/Array/Numeric values.
|
|
437
436
|
# 2. Pattern length ≤ MAX_REGEX_PATTERN_LENGTH (256 chars).
|
|
438
|
-
# 3. Pattern must
|
|
439
|
-
#
|
|
437
|
+
# 3. Pattern must pass Parse::RegexSecurity.validate!, which
|
|
438
|
+
# parses it and refuses repeated groups holding a quantifier,
|
|
439
|
+
# alternation, or backreference, inline comments, and extended
|
|
440
|
+
# mode.
|
|
440
441
|
#
|
|
441
442
|
# For $options:
|
|
442
443
|
#
|
|
443
444
|
# 1. Must be a String.
|
|
444
445
|
# 2. Length ≤ 8 (defensive — real-world usage is 0-3 chars).
|
|
445
|
-
# 3. Every character must appear in ALLOWED_REGEX_OPTIONS (
|
|
446
|
-
# The `
|
|
446
|
+
# 3. Every character must appear in ALLOWED_REGEX_OPTIONS (imsu).
|
|
447
|
+
# The extended `x` flag is rejected.
|
|
447
448
|
#
|
|
448
449
|
# @raise [ConstraintSecurityError] on any rule violation.
|
|
449
450
|
def assert_regex_operand_safe!(op, val)
|
|
@@ -465,12 +466,12 @@ module Parse
|
|
|
465
466
|
reason: :regex_too_long,
|
|
466
467
|
)
|
|
467
468
|
end
|
|
468
|
-
|
|
469
|
+
unless Parse::RegexSecurity.safe?(val, max_length: MAX_REGEX_PATTERN_LENGTH)
|
|
469
470
|
raise ConstraintSecurityError.new(
|
|
470
471
|
"$regex pattern #{val.inspect} contains a nested quantifier " \
|
|
471
|
-
"
|
|
472
|
-
"backtracking on MongoDB's PCRE engine. Rewrite
|
|
473
|
-
"without nested quantifier groups.",
|
|
472
|
+
"or another backtracking-prone construct that can trigger " \
|
|
473
|
+
"catastrophic backtracking on MongoDB's PCRE engine. Rewrite " \
|
|
474
|
+
"the pattern without nested quantifier groups.",
|
|
474
475
|
operator: op,
|
|
475
476
|
reason: :regex_redos,
|
|
476
477
|
)
|
|
@@ -495,8 +496,8 @@ module Parse
|
|
|
495
496
|
raise ConstraintSecurityError.new(
|
|
496
497
|
"$options contains disallowed flag(s) " \
|
|
497
498
|
"#{unrecognized.uniq.inspect}. Allowed flags: " \
|
|
498
|
-
"#{ALLOWED_REGEX_OPTIONS.chars.inspect}. The
|
|
499
|
-
"`
|
|
499
|
+
"#{ALLOWED_REGEX_OPTIONS.chars.inspect}. The extended " \
|
|
500
|
+
"`x` flag is rejected.",
|
|
500
501
|
operator: op,
|
|
501
502
|
reason: :invalid_regex,
|
|
502
503
|
)
|