parse-stack-next 5.8.2 → 5.8.4

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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +323 -0
  3. data/README.md +20 -6
  4. data/bin/parse-console +9 -3
  5. data/docs/TEST_SERVER.md +1 -1
  6. data/docs/caching.md +7 -2
  7. data/docs/mcp_guide.md +34 -0
  8. data/lib/parse/agent/constraint_translator.rb +66 -2
  9. data/lib/parse/agent/mcp_rack_app.rb +90 -1
  10. data/lib/parse/agent/mcp_server.rb +24 -2
  11. data/lib/parse/agent/mcp_subscriptions.rb +32 -1
  12. data/lib/parse/agent/tools.rb +3 -3
  13. data/lib/parse/agent.rb +14 -3
  14. data/lib/parse/atlas_search/protected_paths.rb +163 -0
  15. data/lib/parse/atlas_search.rb +62 -0
  16. data/lib/parse/client.rb +148 -6
  17. data/lib/parse/clp_scope.rb +53 -2
  18. data/lib/parse/embeddings/cache.rb +29 -9
  19. data/lib/parse/graphql/type_generator.rb +184 -5
  20. data/lib/parse/live_query/client.rb +27 -9
  21. data/lib/parse/live_query/event.rb +55 -1
  22. data/lib/parse/live_query/event_queue.rb +6 -1
  23. data/lib/parse/live_query/health_monitor.rb +1 -1
  24. data/lib/parse/model/file.rb +37 -3
  25. data/lib/parse/mongodb.rb +19 -0
  26. data/lib/parse/pipeline_security.rb +242 -5
  27. data/lib/parse/query/constraints.rb +101 -8
  28. data/lib/parse/query/cursor.rb +161 -13
  29. data/lib/parse/query.rb +42 -0
  30. data/lib/parse/retrieval/agent_tool.rb +82 -0
  31. data/lib/parse/retrieval/retriever.rb +14 -2
  32. data/lib/parse/stack/generators/templates/parse.rb +1 -1
  33. data/lib/parse/stack/rack_advisory.rb +53 -0
  34. data/lib/parse/stack/version.rb +1 -1
  35. data/lib/parse/stack.rb +106 -2
  36. data/lib/parse/two_factor_auth/user_extension.rb +41 -5
  37. data/lib/parse/two_factor_auth.rb +81 -4
  38. data/lib/parse/vector_search/hybrid.rb +21 -0
  39. data/lib/parse/vector_search.rb +15 -0
  40. data/lib/parse/webhooks/replay_protection.rb +5 -1
  41. data/lib/parse/webhooks.rb +23 -4
  42. data/parse-stack-next.gemspec +12 -5
  43. metadata +14 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d49d6371c1e6873ea48401f789d9087e23870530ae405bf2629ea9db67360995
4
- data.tar.gz: 9589dfb1ec150a78d527c6ad0097a3ee4263701b104baf7b54afb73e73ec3bdf
3
+ metadata.gz: 278d766a244ba60e85ce4e67d8866df2ce6e6ab78dc76e1f72aedf0b4285d149
4
+ data.tar.gz: 4883c76b576d2a0b726ded45adb1c7a3d569a5d5366f1740da7d44a359d26d53
5
5
  SHA512:
6
- metadata.gz: 22998b27d0f277d174759d29c5087675628eee3a9a24ed460c82c87deda6a28252ed1d1ad78daee9a2438b2a7b1c4f8995197450d9eb5922b5689e58bda52bdb
7
- data.tar.gz: 5ef7ef2f07269ca95bbd985c806250ca7d93d02c7c6bccd83a9d72a0c9a57b990bac63e977ddfbd1c601481562fb12d5c0272639ec415787cef0cc76fc4dd3c0
6
+ metadata.gz: b2a40e323b870080c02613b30b821b87029bb49ddbba9e31581481ac4d13624aae8f456d8bf1fb55f18642fb40d6f73f5e007902b572254f54190df482d6ea80
7
+ data.tar.gz: 2f8e0d0a6d47dafb547e6edb4663ebc2534e24da79760ed81a9ca612cfab8a466f6e9590987401715b0f7edffc61012c4c006f11767ed2a0c33b8cc8e83ecc69
data/CHANGELOG.md CHANGED
@@ -1,5 +1,328 @@
1
1
  ## parse-stack-next Changelog
2
2
 
3
+ ### 5.8.4
4
+
5
+ A security patch across the agent, MCP, Atlas Search, mongo-direct, transport,
6
+ cache, webhook, LiveQuery, GraphQL, MFA, and cursor surfaces, with raised
7
+ dependency floors and a release-workflow fix. Outcomes that change are listed
8
+ under Behavior Notes.
9
+
10
+ #### Agent tools
11
+
12
+ - **FIXED**: `semantic_search` now applies the agent's `classes:` allowlist,
13
+ the CLP `find` check for scoped agents, and the per-agent `filters:` for the
14
+ searched class, as every other read tool does. Before, it checked only the
15
+ global hidden flag and the tenant scope, so an agent limited to one class
16
+ could search any other searchable class, including rows its per-agent filter
17
+ excludes. The per-agent filter is ANDed into the post-search match after the
18
+ caller's filter is checked, so a caller filter on the same field narrows the
19
+ result but cannot replace it. A retrieval profile's hybrid lexical filter is
20
+ likewise ANDed with it instead of merged over it. Per-agent filter fields
21
+ resolve through the class's declared properties, so a declared server name
22
+ such as `PublicState` addresses that column and a pointer uses its `_p_`
23
+ column; a field the class does not declare raises
24
+ `Parse::Agent::ValidationError` instead of being guessed.
25
+ - **FIXED**: A `$relatedTo` constraint built by an agent must name the owning
26
+ object with a plain objectId string. An operator or other non-String value
27
+ (`"objectId" => { "$exists" => true }`) now raises a
28
+ `ConstraintSecurityError` with reason `:invalid_object_id`. Against Parse
29
+ Server below 9.10.5 that shape matches the relation of every owning object
30
+ without checking the owning object's ACL (GHSA-58m4-cqvw-4vgh). The relation
31
+ `key` must be a field name (`:invalid_key`), and a constraint that names
32
+ `object` or `key` under both a String and a Symbol key is refused
33
+ (`:duplicate_key`).
34
+ - **FIXED**: The agent constraint translator refuses a `__type` value (Pointer,
35
+ Date, and the other Parse types) that also carries an operator key, with
36
+ reason `:operator_in_parse_type`. Such a hash was passed through without
37
+ validation, so `{ "__type" => "Pointer", "$relatedTo" => ... }` inside an
38
+ `$or` skipped the operator checks.
39
+
40
+ #### MCP and agent identity
41
+
42
+ - **FIXED**: An agent on a client whose master key is empty or whitespace is
43
+ treated as having no master key, as the HTTP layer already did. Before, it
44
+ took master posture: the `acl_user:` / `acl_role:` and `impersonate`
45
+ refusals were skipped while requests went out without the key.
46
+ - **FIXED**: MCP resource subscriptions apply the agent's tenant scope and
47
+ per-agent filter as the LiveQuery subscription constraint. Before, a
48
+ subscription covered the whole class, so `resources/updated` notifications
49
+ reported writes in other tenants and in rows the filter excludes.
50
+ - **NEW**: A loopback-bound MCP server refuses requests whose `Host` header is
51
+ not a loopback host (DNS-rebinding defense from the MCP specification).
52
+ `allowed_hosts:` on `Parse::Agent::MCPServer` and `MCPRackApp` lists
53
+ additional hosts.
54
+
55
+ #### Atlas Search
56
+
57
+ - **FIXED**: Atlas Search refuses Parse Server's internal columns
58
+ (`_hashed_password`, `_perishable_token`, `_email_verify_token`,
59
+ `_session_token`, `_auth_data_*`, `_rperm`, and the rest of the internal
60
+ field list) when named as a search path, highlight path, sort key,
61
+ autocomplete field, or facet path, for every caller including master. A
62
+ wildcard path glob that matches one of them (`*token`, `?hashed*`) and a
63
+ `queryString` query that names one (`_perishable_token:a*`) are refused the
64
+ same way, including a field name written with Lucene escapes
65
+ (`_hashed\_password:a*`) and globs over the `_auth_data_<provider>`
66
+ columns, and the native hybrid search applies the check to its lexical
67
+ branch. Globs are tested against stored column names only. A glob that
68
+ starts with `_` (which no Parse field does) is refused on every class when
69
+ it reaches a credential column; a glob that starts with a wildcard counts
70
+ credential columns only on `_User` and `_Session`, so `s*` or `*count` on
71
+ another class is not refused. On a dynamically mapped index, a `wildcard` or `regex` operator over
72
+ `_perishable_token` let a caller count matches per prefix and recover a
73
+ password-reset token. On
74
+ `_User` and `_Session`, a non-master caller is also refused a path that
75
+ reaches every field, including a wildcard path object inside a path array,
76
+ since on such an index it includes those columns.
77
+ Refusals raise `Parse::CLPScope::Denied` before the pipeline runs.
78
+
79
+ #### Mongo-direct
80
+
81
+ - **FIXED**: `$jsonSchema` is refused at any depth in mongo-direct filters and
82
+ pipelines, alongside `$where` and `$function`. Its `properties` name fields
83
+ inside the operator value, out of reach of the protectedFields key checks,
84
+ so `{ $jsonSchema: { properties: { email: { pattern: "^a" } } } }` on
85
+ `_User` read a protected field one character at a time. Its `pattern` also
86
+ skipped the SDK's regex checks and the default regex time limit.
87
+ - **FIXED**: `Parse::MongoDB.find` is refused inside `Parse.with_session`
88
+ (with a token or anonymous), as it already was inside
89
+ `Parse.without_master_key`. `find` reads raw documents with no ACL, CLP, or
90
+ protectedFields, so inside a session block it returned every row with
91
+ master authority. A `Parse.with_master_key` block entered inside the session
92
+ block still allows it; one wrapped around the session block does not.
93
+ - **FIXED**: The protectedFields and internal-field checks cover
94
+ `$setWindowFields`, `$densify`, `$fill`, `$bucket`, `$bucketAuto`,
95
+ `$sortByCount`, and a top-level `$search` / `$searchMeta`. A scoped pipeline
96
+ could sort, partition, or fill on a protected field there and read its order.
97
+ Credential columns are refused in those stage arguments for every caller.
98
+ - **FIXED**: The mongo-direct field conversion resolves a pointer's declared
99
+ server name (`belongs_to :owner_ref, field: "OwnerRef"`) to its `_p_OwnerRef`
100
+ column on every path. A constraint keyed by `OwnerRef` reached the wrong
101
+ column, where a `$ne` or `$exists: false` matched every row.
102
+
103
+ #### Schema consistency for scoped reads
104
+
105
+ - **FIXED**: The mongo-direct protected-field check read the class schema
106
+ through the default client and treated an unresolvable schema as having
107
+ no protected fields, while the CLP gate and the protected-field strip read
108
+ it separately through the requesting app's client. Two outcomes followed
109
+ for a scoped caller. If the schema fetch failed and then succeeded between
110
+ the check and the gate, a `$match` on a protected field ran. In a
111
+ multi-app process whose default app had no protectedFields on the class,
112
+ a pipeline such as `$group: { _id: "$ssn" }` returned the protected value,
113
+ since the strip removes only the key. The check now uses the requesting
114
+ app's client, refuses a scoped caller when the schema cannot be resolved,
115
+ and each `Parse::MongoDB.aggregate`, Atlas Search, vector search, and
116
+ hybrid search call reads one schema snapshot for every step.
117
+
118
+ #### Regular-expression validation
119
+
120
+ - **FIXED**: `Parse::RegexSecurity` no longer compiles any part of a caller's
121
+ pattern with Ruby's `Regexp`. Its separator check passed a character class,
122
+ braced escape, or `\Q...\E` atom from the caller's `$regex` to
123
+ `Regexp.new`, so agent and MCP `$regex` arguments, `:field.like` and
124
+ `:field.regex` values, raw `where` hashes, and mongo-direct and Atlas
125
+ Search filters reached Ruby's regular-expression compiler. That compiler
126
+ has memory-safety bugs fixed in Ruby 3.3.13, 3.4.12, and 4.0.9 (GitLab-assigned
127
+ CVE-2026-89078, CVE-2026-93577, CVE-2026-97229, CVE-2026-110264), and
128
+ `\Q...\E` reached it as live syntax because it does not support that
129
+ construct. Character membership is now decided from the validator's own
130
+ parse (literals, `\d \w \s` and their negations, ranges); any construct
131
+ it cannot evaluate counts as a possible match, which refuses the pattern
132
+ rather than passing it. Patterns are still sent to Parse Server and MongoDB
133
+ as strings and compiled only by MongoDB's PCRE engine.
134
+
135
+ #### Transport and caching
136
+
137
+ - **FIXED**: `Parse::Client` refuses an absolute (`https://host/...`, any case)
138
+ or protocol-relative (`//host`, `\\host`) request path with `ArgumentError`
139
+ before sending, so a caller-supplied path cannot send the master key or a
140
+ session token to another host. `connected?` raises on such a path.
141
+ - **FIXED**: `PARSE_REQUIRE_HTTPS` accepts `true`, `1`, or `yes` in any case.
142
+ Before, only the exact string `true` enabled it.
143
+ - **FIXED**: Credentials embedded in the server URL (`https://user:pass@...`)
144
+ are shown as `[FILTERED]` in `inspect`, `as_json`, and warnings, with a
145
+ one-time warning that the URL contains them.
146
+ - **CHANGED**: A `cache:` store that deserializes with Marshal (Moneta's
147
+ default value serializer) over an external backend is refused with
148
+ `ArgumentError`; anyone able to write to that backend could run code on each
149
+ cache read. Use `Parse::Cache::Redis` (or a `redis://` URL string), an
150
+ in-process store, or pass `allow_marshal_cache: true` to accept the risk. The embedding cache's `MonetaStore` applies the same rule with
151
+ `allow_marshal: true`. In-process stores (`:Memory`, `:LRUHash`, `:Null`)
152
+ are accepted.
153
+
154
+ #### File fetches
155
+
156
+ - **FIXED**: Remote `Parse::File` fetches also refuse the NAT64
157
+ (`64:ff9b::/96`) and 6to4 (`2002::/16`) ranges, which embed an IPv4
158
+ address that can be internal on networks that route them.
159
+
160
+ #### Webhooks and LiveQuery
161
+
162
+ - **FIXED**: A whitespace-only webhook signing secret counts as unset, so it
163
+ can no longer serve as a guessable HMAC key that grants master trust. A blank
164
+ `PARSE_SERVER_WEBHOOK_KEY` no longer hides `PARSE_WEBHOOK_KEY`.
165
+ - **FIXED**: The webhook handler refuses a declared `Content-Length` over 1 MB
166
+ before reading, and reads at most 1 MB plus one byte. Before, it read the
167
+ whole body and then checked the size.
168
+ - **FIXED**: LiveQuery events for `_User` and `_Session` have `sessionToken`,
169
+ `authData`, and other credential columns removed before hydration. On a
170
+ Parse Server affected by GHSA-349r-988w-x3jp, an `$or` subscription could
171
+ receive other users' events carrying their session tokens.
172
+
173
+ #### Ambient identity lifetime
174
+
175
+ - **NEW**: `Parse.with_session` documents its identity contract: the session
176
+ token and master-key state last for the block on the current fiber and are
177
+ restored on exit, including on exception, so reused request threads carry
178
+ nothing between requests. Threads and Fibers created inside the block
179
+ inherit the identity for their whole lifetime, which suits short-lived
180
+ work for that user. `Parse.without_ambient_identity { }` runs a block with
181
+ no inherited identity, for workers started inside a session block.
182
+ - **FIXED**: LiveQuery's event, reader, reconnect, and health-check threads
183
+ start without the caller's ambient identity, so callbacks no longer run
184
+ with a session token inherited from whichever block first started the
185
+ client. A thread started from restricted code (a session block or
186
+ `Parse.without_master_key`) keeps the master key suppressed, so callbacks
187
+ never gain authority the starting code lacked;
188
+ `Parse.without_ambient_identity(keep_restrictions: true)` applies the same
189
+ rule to application workers. A reconnect scheduled inside `Parse.without_master_key` keeps the
190
+ master key withheld.
191
+
192
+ #### GraphQL, MFA, and cursors
193
+
194
+ - **FIXED**: `Parse::GraphQL::TypeGenerator` never emits credential fields
195
+ (`session_token`, `auth_data`, `password`, `device_token`, and the internal
196
+ field list). Field resolution bypassed `as_json` redaction, so a generated
197
+ `Session` type served bearer tokens. `include_sensitive: [:field]` opts a
198
+ field back in.
199
+ - **NEW**: The generator takes an explicit field policy: `exclude_fields:`
200
+ (an Array, or a Hash per class) and `protected_fields:`, either a Hash per
201
+ class or `:fetch`, which loads each class's public protectedFields from
202
+ the server and raises `ProtectedFieldsUnavailable` if it cannot. Without
203
+ those options it reads no CLP at all, so the same models always produce
204
+ the same types. Generated types are a schema shape, not an authorization
205
+ layer: resolvers must still enforce ACL, CLP, and protectedFields for the
206
+ requesting user.
207
+ - **FIXED**: `mfa_required?` requires a String username. A Hash value became
208
+ a query operator run under the master key, which let a pre-login endpoint
209
+ report MFA status for an arbitrary matching account.
210
+ - **NEW**: `Parse::MFA.verify(secret, code, after:)` passes `after:` to ROTP,
211
+ and `Parse::MFA.verify_timestamp` returns the accepted time step, so an
212
+ application using `verify` as its own second factor can refuse a code used
213
+ again within the drift window.
214
+ - **NEW**: `Parse::Cursor` state can be signed with HMAC-SHA256
215
+ (`Parse::Cursor.signing_secret=`, or `secret:`), and
216
+ `deserialize(json, expected_class:)` refuses a different class. A signing
217
+ secret must be a String of at least 32 bytes; a blank, short, or non-String
218
+ secret raises `ArgumentError`. Length does not make a key strong, so the
219
+ documentation calls for 32 bytes from a cryptographic random source
220
+ (`SecureRandom.hex(32)`). With a secret configured, `trusted: true` does
221
+ not bypass signature verification. Unsigned state is meant only for trusted
222
+ persistence (state the process stored where clients cannot change it):
223
+ pass `trusted: true` to load it silently. Without that flag it still loads
224
+ in this release with a one-time warning; a future minor release will
225
+ require it. Saved date positions now round-trip as dates.
226
+ - **CHANGED**: `parse-console -m` warns that a master key on the command line
227
+ is visible in the process list and shell history.
228
+
229
+ #### Documentation
230
+
231
+ - **CHANGED**: The MCP guide states that the per-agent rate limiter does not
232
+ limit across requests when an agent is built per request, and that
233
+ deployments must enforce limits per client at the proxy or gateway.
234
+ - **CHANGED**: The MFA documentation states that `Parse::MFA.verify` and the
235
+ SDK's MFA-disable flow are client-side checks, that Parse Server is the
236
+ enforcement boundary (disabling MFA must be guarded server-side), that
237
+ replay protection needs an atomic compare-and-set on the stored
238
+ timestamp, and that `authorized_by:` must be the authenticated operator.
239
+ - **CHANGED**: `Parse::File` documents that a file built from a URL records
240
+ the remote server's Content-Type, which is attacker-controlled when the URL
241
+ is, and that applications decoding stored files must sniff content and
242
+ disable untrusted decoders (for libvips, `VIPS_BLOCK_UNTRUSTED` or
243
+ `Vips.block_untrusted(true)`).
244
+
245
+ #### Dependencies, CI, and release workflow
246
+
247
+ - **CHANGED**: The gemspec requires `faraday` 2.14.3 or later (CVE-2026-54297,
248
+ CVE-2026-33637, GHSA-33mh-2634-fwr2). The 2026-04 rack advisories are fixed
249
+ per branch in 2.2.23, 3.1.21, and 3.2.6, with no fixed 3.0 release. The
250
+ `rack` requirement is 2.2.23 or later, and loading the SDK on a vulnerable
251
+ Rack 3.x release (3.0.x, 3.1.0 to 3.1.20, or 3.2.0 to 3.2.5) prints a
252
+ one-time warning naming the fixed release. RubyGems limits a requirement
253
+ string to 255 characters, too short to exclude each of those releases by
254
+ name. Rack 2.2 applications remain supported. The optional `mongo` guidance is 2.26 or later (CVE-2026-88030)
255
+ and `graphql` 2.6.9 or later.
256
+ - **CHANGED**: The integration test stack runs Parse Server 9.10.7 and Atlas
257
+ Local 8.0.34. CI uses `ruby/setup-ruby` 1.334.0, which installs the patched
258
+ Ruby 3.3.13, 3.4.12, and 4.0.9 for those matrix legs, and pins every action
259
+ to a commit SHA. The Ruby 3.2 leg remains for legacy compatibility; 3.2 is
260
+ end of life and has no release with the October 2026 regular-expression
261
+ compiler fixes.
262
+ - **FIXED**: The release workflow waits up to 45 minutes for Gem Tests on the
263
+ release commit to be created and finish before judging it. A release
264
+ published right after its merge commit landed failed the gate because the
265
+ tests on that commit had not finished yet.
266
+ - **CHANGED**: The local Ruby version is now 3.4.12, which includes the
267
+ October 2026 regular-expression compilation fixes.
268
+ - **NEW**: Gem Tests runs `bundle exec bundle-audit check --update` against
269
+ the committed lockfile on pull requests and pushes to main. Known gem
270
+ vulnerabilities or an advisory-database update failure fail the check,
271
+ and the release workflow's Gem Tests gate includes this job.
272
+
273
+ #### Behavior Notes
274
+
275
+ - A `cache:` store with Moneta's default Marshal serializer over a file or
276
+ network backend now raises `ArgumentError` at `Parse.setup`.
277
+ - `Parse::Client#get`/`post`/`put`/`delete` and `connected?` raise
278
+ `ArgumentError` for an absolute or protocol-relative path.
279
+ - A loopback-bound MCP server refuses requests whose `Host` is not a loopback
280
+ host or listed in `allowed_hosts:`.
281
+ - An application subscribed to its own `_User` through LiveQuery no longer
282
+ receives its `sessionToken` in events.
283
+ - `faraday` below 2.14.3 and `rack` below 2.2.23 no longer satisfy the
284
+ gemspec. A vulnerable `rack` 3.x release still installs and prints a
285
+ load-time warning.
286
+ - `Parse::Cursor` signing secrets shorter than 32 bytes, blank, or not
287
+ Strings raise `ArgumentError`.
288
+ - `mfa_required?(nil)` and other non-String usernames raise `ArgumentError`
289
+ instead of returning false.
290
+ - A per-agent filter (including a `:default` filter) that names a field the
291
+ searched class does not declare makes `semantic_search` raise
292
+ `Parse::Agent::ValidationError`; the other read tools return no rows in
293
+ that case.
294
+ - A scoped mongo-direct, Atlas Search, or vector search call on a class
295
+ whose schema cannot be resolved is refused before it runs.
296
+ - Applications that compile regular expressions from untrusted input
297
+ themselves should run Ruby 3.3.13, 3.4.12, or 4.0.9 or later. The SDK no
298
+ longer does so. Ruby 3.2 remains supported for legacy compatibility only:
299
+ it is end of life and does not receive these fixes.
300
+ - An agent `call_method` handler that runs under a session-scoped agent is
301
+ executed inside `Parse.with_session`, so a handler that calls
302
+ `Parse::MongoDB.find` now raises `Parse::ACLScope::ACLRequired`. Use
303
+ `Parse::Query#results_direct`, which applies the ambient session,
304
+ `Parse::MongoDB.aggregate` with an explicit `session_token:`, or wrap a
305
+ deliberate master read in `Parse.with_master_key`.
306
+ - A pipeline or filter that used `$jsonSchema` on the mongo-direct path now
307
+ raises `Parse::PipelineSecurity::Error` (or
308
+ `Parse::MongoDB::DeniedOperator`).
309
+ - A non-master Atlas Search over `_User` or `_Session` with no `fields:` is
310
+ refused; name the fields to search.
311
+
312
+ ### 5.8.3
313
+
314
+ A maintenance release that refreshes locked dependencies to their latest
315
+ compatible versions. No SDK behavior changes.
316
+
317
+ #### Dependency updates
318
+
319
+ - **CHANGED**: Bumped locked dependencies to their latest compatible releases:
320
+ `faraday` 2.14.4 → 2.15.0, `faraday-net_http` 3.4.4 → 3.5.0, `graphql`
321
+ 2.6.11 → 2.6.13, `json` 3.0.2 → 3.1.0, `net-http` 0.9.1 → 0.10.0,
322
+ `prettyprint` 0.2.0 → 0.2.1, `pry` 0.16.0 → 0.16.1, `rack-test` 2.2.0 →
323
+ 2.3.0, and `uri` 1.1.1 → 1.1.2. `net-http` 0.10.0 adds `net-protocol` 0.4.0
324
+ and `timeout` 0.6.1 as new transitive dependencies.
325
+
3
326
  ### 5.8.2
4
327
 
5
328
  A security and correctness patch. No new features beyond the `session:`
data/README.md CHANGED
@@ -102,7 +102,7 @@ See [CHANGELOG.md](./CHANGELOG.md) for the full 5.1 entry, including breaking ch
102
102
  - **MCP transport hardening** — Streamable HTTP `Mcp-Session-Id` header (renamed from `X-MCP-Session-Id`, **breaking**), `MCP-Protocol-Version` validation, `DELETE /` session termination, structured-content (`outputSchema`) on built-in tools, optional `health_path:` liveness probe
103
103
  - **`Parse::GraphQL::TypeGenerator`** — generate `graphql-ruby` types directly from your `Parse::Object` subclasses (no Parse Server round-trip), with `:vector` columns surfaced as `[Float]` and association registries (`has_one_associations`, `has_many_associations`) populated at DSL time
104
104
  - **LiveQuery promoted to stable** — the experimental warning is removed; `Parse.live_query_enabled = true` is retained as a network-egress safety toggle, not a stability gate
105
- - **Server-version deprecation warning**: a one-shot warning when connecting to a Parse Server older than the configured threshold (default `7.0.0`, override with `PARSE_DEPRECATED_SERVER_VERSION_BELOW`); silence with `Parse.suppress_server_version_warning = true`. The **supported baseline is Parse Server 9.x** (the SDK is developed and tested against a pinned `parse-server:9.10.3`); the default warning threshold is intentionally conservative so older deployments only get an advisory, not a hard break.
105
+ - **Server-version deprecation warning**: a one-shot warning when connecting to a Parse Server older than the configured threshold (default `7.0.0`, override with `PARSE_DEPRECATED_SERVER_VERSION_BELOW`); silence with `Parse.suppress_server_version_warning = true`. The **supported baseline is Parse Server 9.x** (the SDK is developed and tested against a pinned `parse-server:9.10.7`); the default warning threshold is intentionally conservative so older deployments only get an advisory, not a hard break.
106
106
  - **`mongo_relation_index :field, dedup: true`** — register a compound `{owningId, relatedId}` UNIQUE on relation join collections to prevent duplicate-pair subscriptions without breaking `has_many` semantics
107
107
 
108
108
  See [CHANGELOG.md](./CHANGELOG.md) for the full 5.0 entry, including security-hardening notes and Ruby 3.x cleanup.
@@ -286,7 +286,7 @@ Runnable, self-contained scripts live in [`examples/`](examples/) — see
286
286
 
287
287
  ## Release History
288
288
 
289
- **Current version: 5.5.5** | **Ruby 3.2+ required**
289
+ **Current version: 5.8.4** | **Ruby 3.2+ required** (3.3.13, 3.4.12, or 4.0.9 or later recommended for their regular-expression compiler fixes)
290
290
 
291
291
  The 5.0 highlights (vector search / RAG, pooled Redis cache, AS::N instrumentation, MCP transport hardening, GraphQL type generation) are summarized in the [What's new in 5.0](#whats-new-in-50) section above. Earlier releases are recorded below.
292
292
 
@@ -689,9 +689,13 @@ A caching adapter of type `Moneta::Transformer`. Caching queries and object fetc
689
689
  Parse.setup(cache: store, expires: 10, ...)
690
690
  ```
691
691
 
692
- If you supply your own raw `Moneta.new(:Redis, ...)` store instead of the
693
- wrapper, build it with `value_serializer: nil` to keep Marshal off the cache
694
- read path.
692
+ Use the wrapper rather than a raw `Moneta.new(:Redis, ...)` store. A store that
693
+ keeps Moneta's default Marshal serializer over an external backend is refused
694
+ with `ArgumentError` unless you pass `allow_marshal_cache: true`. Setting
695
+ `value_serializer: nil` on a raw store is not a substitute: the response cache
696
+ stores Hash values, which a file or Redis backend cannot hold without a
697
+ serializer, so every lookup misses. In-process stores (`:Memory`, `:LRUHash`,
698
+ `:Null`) are accepted as they are.
695
699
 
696
700
  As a shortcut, if you are planning on using REDIS and have configured the use of `redis` in your `Gemfile`, you can just pass the REDIS connection string directly to the cache option. The string form builds a `Parse::Cache::Redis` wrapper for you, so it is JSON-serialized and safe by default.
697
701
 
@@ -1220,6 +1224,8 @@ The default MIME type for all files is `image/jpeg`. This can be default can be
1220
1224
  file = Parse::File.new parse_file
1221
1225
  ```
1222
1226
 
1227
+ A URL String passed to `Parse::File.new` or `Parse::File.create`, or assigned to a `:file` property, is downloaded and the remote server's `Content-Type` header is recorded as the file's mime type. That type is controlled by whoever controls the URL. Do not assign request strings to file properties; build files from bytes (`Parse::File.new(name, bytes, type)`) and determine the type server-side. Code that decodes stored files (libvips or ImageMagick, Active Storage variants, document parsers) should treat the stored mime type as untrusted, sniff the content, and disable untrusted decoders (for libvips 8.13 and later, `VIPS_BLOCK_UNTRUSTED` or `Vips.block_untrusted(true)` with ruby-vips 2.2.1 and later). Keep `Parse::File.allowed_remote_hosts` restricted to expected hosts.
1228
+
1223
1229
  If you are using displaying these files on a secure site and want to make sure that urls returned by a call to `url` are `https`, you can set `Parse::File.force_ssl` to true.
1224
1230
 
1225
1231
  ```ruby
@@ -1955,6 +1961,14 @@ user.disable_mfa_master_key!(authorized_by: admin_user, admin_role: "Admin")
1955
1961
  # or: user.disable_mfa_master_key!(authorized_by: admin_user, allow_unverified: true)
1956
1962
  ```
1957
1963
 
1964
+ **MFA enforcement boundary:**
1965
+
1966
+ `Parse::MFA.verify`, `Parse::MFA.verify_timestamp` and `disable_mfa!` are client-side checks. Parse Server's MFA adapter enforces MFA at login and is the enforcement boundary. Removing MFA is a user update (`authData: { mfa: nil }`) that any holder of the session token can send to the REST API directly, so protect it server-side, for example with a `beforeSave` trigger on `_User` or a Cloud Code function that requires a fresh code verified on the server.
1967
+
1968
+ When using `Parse::MFA.verify(secret, code, after:)` as an application's own second factor, store the last accepted timestamp per user and update it atomically: a compare-and-set keyed on the previous value, or a database unique constraint on user and time step. Otherwise two concurrent requests with the same code can both succeed.
1969
+
1970
+ `authorized_by:` on `disable_mfa_master_key!` must be the authenticated operator from the caller's verified session, never a user id taken from the request.
1971
+
1958
1972
  **SMS MFA (requires Parse Server SMS callback):**
1959
1973
  ```ruby
1960
1974
  # Initiate SMS setup
@@ -5735,7 +5749,7 @@ For high traffic applications that may be performing several server tasks on sim
5735
5749
  # Parse::Cache::Redis serializes cached responses as JSON, not Marshal — a raw
5736
5750
  # Moneta.new(:Redis) store Marshals values by default and a cache read would
5737
5751
  # Marshal.load Redis bytes (RCE if the cache is shared/untrusted). Prefer the
5738
- # wrapper; if you supply a raw Moneta-Redis store, pass value_serializer: nil.
5752
+ # wrapper over a raw Moneta-Redis store.
5739
5753
  store = Parse::Cache::Redis.new(url: 'redis://localhost:6379')
5740
5754
  # use a Redis cache store with an automatic expire of 10 seconds.
5741
5755
  Parse.setup(cache: store, expires: 10, ...)
data/bin/parse-console CHANGED
@@ -111,13 +111,19 @@ opt_parser = OptionParser.new do |o|
111
111
  o.separator "#{File.basename($0)} -a APP_ID -m MASTER_KEY https://your-parse-server-url"
112
112
  o.separator "#{File.basename($0)} -c ./config.json"
113
113
  o.separator ""
114
- o.separator "If you use the -m option, parse-console will automatically"
114
+ o.separator "With a master key (-m or -c), parse-console will automatically"
115
115
  o.separator "import your schema as ruby models."
116
116
  o.separator ""
117
117
  o.separator "Options"
118
118
  o.on('-a APP_ID', '--appId APP_ID', 'Parse App ID (required)') { |a| opts[:app_id] = a }
119
119
  o.on('-k REST_API_KEY', '--restAPIKey REST_API_KEY', 'Parse REST API Key') { |a| opts[:api_key] = a }
120
- o.on('-m MASTER_KEY', '--masterKey MASTER_KEY', 'Parse Master Key') { |a| opts[:master_key] = a }
120
+ o.on('-m MASTER_KEY', '--masterKey MASTER_KEY', 'Parse Master Key (prefer -c CONFIG_JSON)') do |a|
121
+ # A key on the command line is visible to other local users through the
122
+ # process list and is saved in shell history. Keep accepting it, but say so.
123
+ $stderr.puts "Warning: passing the master key with -m exposes it in the process list " \
124
+ "and shell history. Put it in a config file and use -c CONFIG_JSON instead."
125
+ opts[:master_key] = a
126
+ end
121
127
  o.on('-s SERVER_URL', '--serverURL SERVER_URL', 'The Parse server url.', 'Defaults to http://localhost:1337/parse') { |a| opts[:server_url] = a }
122
128
  o.on('-v','--[no-]verbose', 'Run verbosely') { |v| opts[:verbose] ||= v }
123
129
  o.on('--pry', 'Use Pry instead of IRB') { |v| opts[:pry] = v }
@@ -212,7 +218,7 @@ Parse.setup server_url: opts[:server_url],
212
218
  master_key: opts[:master_key]
213
219
  Parse.logging = true if opts[:verbose]
214
220
  # Both of these can have come from a remote config document, so escape them.
215
- puts "Server : #{Parse::TerminalSafe.sanitize_line(Parse.client.server_url)}"
221
+ puts "Server : #{Parse::TerminalSafe.sanitize_line(Parse::Client.redact_url_userinfo(Parse.client.server_url))}"
216
222
  puts "App Id : #{Parse::TerminalSafe.sanitize_line(Parse.client.app_id)}"
217
223
  puts "Master : #{Parse.client.master_key.present?}"
218
224
 
data/docs/TEST_SERVER.md CHANGED
@@ -62,7 +62,7 @@ default.
62
62
  - **`PSNEXT_PREFIX`** (default `psnext-it`) names the Compose project and every
63
63
  container. Set it (e.g. `PSNEXT_PREFIX=psnext-ci`) to run a second, fully
64
64
  separate copy.
65
- - **Versions**: Parse Server is pinned to `parseplatform/parse-server:9.10.3`
65
+ - **Versions**: Parse Server is pinned to `parseplatform/parse-server:9.10.7`
66
66
  (see `scripts/docker/Dockerfile.parse`), MongoDB `mongo:9` (`MONGO_VERSION`), Redis
67
67
  `redis:7-alpine`, Dashboard `parseplatform/parse-dashboard:9`.
68
68
  - **Database**: Parse uses `parse_stack_next_it`.
data/docs/caching.md CHANGED
@@ -87,8 +87,13 @@ Use the wrapper rather than a bare `Moneta.new(:Redis, ...)`. Moneta serializes
87
87
  values with Marshal by default, so every cache read would `Marshal.load` bytes
88
88
  returned by Redis, which is a remote-code-execution primitive when that Redis is
89
89
  shared, unauthenticated, or reachable over a plaintext connection. The wrapper
90
- forces `value_serializer: nil` and encodes values as JSON itself. If you supply
91
- your own Moneta store, build it with `value_serializer: nil`.
90
+ forces `value_serializer: nil` and encodes values as JSON itself. A raw Moneta
91
+ store that keeps the Marshal serializer over an external backend is refused with
92
+ `ArgumentError` unless you pass `allow_marshal_cache: true`; in-process stores
93
+ (`:Memory`, `:LRUHash`, `:Null`) are accepted. Setting `value_serializer: nil` on
94
+ a raw file or Redis store is not a substitute for the wrapper: the response
95
+ cache stores Hash values, which such a backend cannot hold without a
96
+ serializer, so every lookup misses.
92
97
 
93
98
  The wrapper refuses a Moneta `prefix:` option, because it would rewrite the
94
99
  physical key layout underneath the SCAN patterns that scoped clearing depends
data/docs/mcp_guide.md CHANGED
@@ -1424,6 +1424,40 @@ The `Parse::Agent::RateLimitExceeded` constant is a stable top-level alias — e
1424
1424
 
1425
1425
  Per-user rate limiting follows the same pattern: key the Redis counter on the verified user identity extracted during authentication.
1426
1426
 
1427
+ ### Deployment-layer limits
1428
+
1429
+ The SDK's limiters are not a substitute for limits at the deployment layer.
1430
+
1431
+ - `Parse::Agent::RateLimiter` is created per `Parse::Agent` instance. With an `agent_factory:` that builds an agent per request, it does not limit anything across requests.
1432
+ - `MCPServer` keeps one limiter for the whole server. Every principal (every API key holder, every client IP) draws from that one budget, so a single noisy client can exhaust it for all others, and it does not limit any one client.
1433
+ - `pre_auth_rate_limiter:` is called as `check!` with no arguments, before authentication. It sees no client identity, so it limits total request volume rather than any one caller.
1434
+
1435
+ Production deployments should enforce a per-client limit (keyed on API key, user, or IP) at a reverse proxy or API gateway in front of the MCP endpoint, and also pass a shared `pre_auth_rate_limiter:` so unauthenticated traffic cannot drive unbounded Parse Server session lookups. Back the limiter with a shared store (for example Redis) when more than one process serves the endpoint.
1436
+
1437
+ ```ruby
1438
+ $pre_auth_limiter = MyRedisRateLimiter.new(key: "mcp_pre_auth", limit: 600, window: 60)
1439
+
1440
+ # Rack mount (MCPRackApp, or Parse::Agent.rack_app which forwards kwargs)
1441
+ mcp_app = Parse::Agent::MCPRackApp.new(
1442
+ pre_auth_rate_limiter: $pre_auth_limiter,
1443
+ agent_factory: ->(env) {
1444
+ # ... auth ...
1445
+ Parse::Agent.new(permissions: :readonly, rate_limiter: $shared_redis_limiter)
1446
+ },
1447
+ )
1448
+
1449
+ # Standalone server. MCPServer.run does not forward pre_auth_rate_limiter:,
1450
+ # so construct the server directly.
1451
+ Parse::Agent::MCPServer.new(
1452
+ port: 3001,
1453
+ api_key: ENV["MCP_API_KEY"],
1454
+ rate_limiter: $shared_redis_limiter,
1455
+ pre_auth_rate_limiter: $pre_auth_limiter,
1456
+ ).start
1457
+ ```
1458
+
1459
+ On exhaustion the pre-auth limiter must raise an exception responding to `#retry_after` (such as `Parse::Agent::RateLimitExceeded`); the request is answered with HTTP 429 and a `Retry-After` header.
1460
+
1427
1461
  ---
1428
1462
 
1429
1463
  ## Custom Tools
@@ -233,8 +233,18 @@ module Parse
233
233
 
234
234
  # Translate a hash value (could be operators or a pointer/object)
235
235
  def translate_hash_value(hash, depth:, agent: nil)
236
- # Check if it's a Parse type (Pointer, Date, File, GeoPoint)
237
- return hash if parse_type?(hash)
236
+ # Check if it's a Parse type (Pointer, Date, File, GeoPoint). A Parse
237
+ # type value carries no operators, so a `__type` hash that also has a
238
+ # `$` key is refused instead of being passed through unvalidated.
239
+ if parse_type?(hash)
240
+ op_key = hash.keys.find { |k| k.to_s.start_with?("$") }
241
+ return hash if op_key.nil?
242
+ raise ConstraintSecurityError.new(
243
+ "SECURITY: a `__type` value cannot also carry the operator #{op_key}.",
244
+ operator: op_key.to_s,
245
+ reason: :operator_in_parse_type,
246
+ )
247
+ end
238
248
 
239
249
  # Classify each key INDEPENDENTLY rather than branching on whether
240
250
  # *every* key is an operator. A hash that mixes an operator key with a
@@ -366,10 +376,64 @@ module Parse
366
376
  reason: :cross_class_denied,
367
377
  )
368
378
  end
379
+ # The owning object must be one concrete object. An operator or any
380
+ # other non-String objectId (`{ "$exists" => true }`) makes Parse
381
+ # Server below 9.10.5 match relations of every owning object, without
382
+ # the owning object's ACL (GHSA-58m4-cqvw-4vgh).
383
+ # A Hash holding both "object" and :object (or "key" and :key) would
384
+ # let the checks read one entry while the other is sent.
385
+ %w[object key].each do |slot|
386
+ next unless val.is_a?(Hash) && val.key?(slot) && val.key?(slot.to_sym)
387
+ raise ConstraintSecurityError.new(
388
+ "SECURITY: $relatedTo names `#{slot}` twice (String and Symbol keys).",
389
+ operator: "$relatedTo",
390
+ reason: :duplicate_key,
391
+ )
392
+ end
393
+ key = val["key"] || val[:key]
394
+ unless key.is_a?(String) && RELATED_TO_KEY_RE.match?(key)
395
+ raise ConstraintSecurityError.new(
396
+ "SECURITY: $relatedTo requires `key` to be a relation field name.",
397
+ operator: "$relatedTo",
398
+ reason: :invalid_key,
399
+ )
400
+ end
401
+ object_id = related_to_object_id(val)
402
+ unless object_id.is_a?(String) && RELATED_TO_OBJECT_ID_RE.match?(object_id)
403
+ raise ConstraintSecurityError.new(
404
+ "SECURITY: $relatedTo requires the owning object's objectId to be a " \
405
+ "plain objectId string.",
406
+ operator: "$relatedTo",
407
+ reason: :invalid_object_id,
408
+ )
409
+ end
369
410
  assert_embedded_class_accessible!("$relatedTo", owning_class, agent: agent)
370
411
  translate_value(val, depth: depth, agent: agent)
371
412
  end
372
413
 
414
+ # objectId shape accepted for a `$relatedTo` owning object (the same
415
+ # character set the agent tools accept for an object_id argument).
416
+ RELATED_TO_OBJECT_ID_RE = /\A[A-Za-z0-9_-]{1,32}\z/.freeze
417
+
418
+ # Field-name shape accepted for a `$relatedTo` relation key.
419
+ RELATED_TO_KEY_RE = /\A[A-Za-z][A-Za-z0-9_]{0,127}\z/.freeze
420
+
421
+ # The objectId of a `$relatedTo` constraint's owning object, in
422
+ # whichever of the forms {.related_to_owning_class} accepts. Returns
423
+ # the raw value (possibly a Hash) so the caller can reject non-Strings.
424
+ def related_to_object_id(val)
425
+ return nil unless val.is_a?(Hash)
426
+ obj = val["object"] || val[:object]
427
+ return obj.id if obj.respond_to?(:parse_class) && obj.respond_to?(:id)
428
+ case obj
429
+ when Hash
430
+ o = obj.transform_keys(&:to_s)
431
+ o["objectId"]
432
+ when String
433
+ obj.include?("$") ? obj.split("$", 2).last : nil
434
+ end
435
+ end
436
+
373
437
  # Extract the Parse class name of a `$relatedTo` constraint's owning
374
438
  # object from its `object` slot, which may be a Parse::Pointer, a Parse
375
439
  # pointer/relation hash (`{__type:, className:, objectId:}`, string or