|
1 | 1 | ## parse-stack-next Changelog |
2 | 2 |
|
| 3 | +### 5.5.1 |
| 4 | + |
| 5 | +#### Mongo-direct reads inside `Parse.with_session` are now scoped, not master |
| 6 | + |
| 7 | +- **FIXED**: A query that auto-routes to the mongo-direct path because of a |
| 8 | + direct-only constraint (for example a geo `$near` / `$geoIntersects` query) |
| 9 | + now honors the ambient session token set by `Parse.with_session(token)`. |
| 10 | + Previously the mongo-direct auth resolver consulted only the query's own |
| 11 | + `session_token=` / `scope_to_user` / `scope_to_role` and ignored the |
| 12 | + fiber-local ambient session, so in server mode it fell through to a |
| 13 | + master-key read with no ACL/CLP enforcement — returning rows the session was |
| 14 | + not permitted to see, even though every REST query in the same |
| 15 | + `with_session` block was correctly scoped. The resolver now mirrors |
| 16 | + `Parse::Client#request` precedence: an explicit per-query token wins, then |
| 17 | + the ambient session, then the master-key fallback; an explicit |
| 18 | + `use_master_key: true` is a deliberate admin call and still skips the |
| 19 | + ambient. Routing also accepts the ambient on non-master clients |
| 20 | + (`Parse.client_mode` or a user-scoped client), so such a query runs scoped |
| 21 | + rather than raising. |
| 22 | + |
| 23 | +#### Boolean property coercion no longer treats the string "false" as true |
| 24 | + |
| 25 | +- **FIXED**: A `:boolean` property assigned a string now coerces via |
| 26 | + ActiveModel's boolean caster instead of raw Ruby truthiness. Previously the |
| 27 | + coercion was `val ? true : false`, so the strings `"false"`, `"0"`, and |
| 28 | + `"off"` — exactly what arrives on a Rails-form or query-string ingestion |
| 29 | + path — all coerced to `true`, silently flipping a boolean the wrong way (for |
| 30 | + example an `archived` flag or an application-defined access gate). String |
| 31 | + forms now map correctly (`"false"`/`"0"`/`"off"` to `false`), a blank string |
| 32 | + is treated as unset (`nil`), and native booleans from Parse wire JSON pass |
| 33 | + through unchanged. |
| 34 | + |
| 35 | +#### Deprecation warning for setting ACL via mass-assignment |
| 36 | + |
| 37 | +- **DEPRECATED**: Setting `acl`/`ACL` through mass-assignment |
| 38 | + (`Parse::Object#attributes=`) now emits a one-time security warning. Mass- |
| 39 | + assigning an ACL from a caller-supplied hash — for example a controller doing |
| 40 | + `record.attributes = params` without StrongParameters — lets an attacker |
| 41 | + grant unintended access by sending an `ACL` key |
| 42 | + (`{"ACL" => {"*" => {"write" => true}}}`). The behavior is unchanged this |
| 43 | + release (the ACL is still applied), but the supported path is the explicit |
| 44 | + `record.acl = ...` setter, and a future release may block ACL mass-assignment. |
| 45 | + The constructor form `Klass.new(acl: ...)` is unaffected and does not warn. |
| 46 | + |
| 47 | +#### Redis cache values serialized as JSON instead of Marshal |
| 48 | + |
| 49 | +- **FIXED**: `Parse::Cache::Redis` now serializes cached HTTP responses as |
| 50 | + JSON rather than Marshal. The Moneta-Redis store Marshals values by default, |
| 51 | + so every cache hit ran `Marshal.load` on the bytes returned by Redis. Against |
| 52 | + a shared, unauthenticated, or plaintext-`redis://` cache, an attacker able to |
| 53 | + write the cache could plant a crafted Marshal payload that executed code on |
| 54 | + deserialization. The wrapper now disables Moneta's value serializer |
| 55 | + (`value_serializer: nil`) and JSON-encodes/decodes values itself; an |
| 56 | + undecodable value (including any legacy Marshal entry) is treated as a cache |
| 57 | + miss rather than deserialized. Cache keys are unchanged. No application code |
| 58 | + changes are required; existing cached entries are transparently refetched and |
| 59 | + re-stored in the new format on first access. |
| 60 | +- **FIXED**: The `cache: "redis://..."` shorthand on `Parse::Client.new` / |
| 61 | + `Parse.setup` now builds a `Parse::Cache::Redis` store instead of a bare |
| 62 | + `Moneta.new(:Redis, ...)`, so it gets the same JSON value serialization and |
| 63 | + is not subject to the Marshal deserialization issue above. |
| 64 | +- **CHANGED**: The caching middleware stores response entries with string keys |
| 65 | + so they round-trip losslessly through the JSON serialization. Reads accept |
| 66 | + both string and legacy symbol keys. |
| 67 | +- **FIXED**: `Parse::Embeddings::Cache::MonetaStore` now JSON-encodes cached |
| 68 | + embedding vectors instead of relying on the Moneta store's default Marshal |
| 69 | + value serializer, closing the same `Marshal.load`-on-read deserialization |
| 70 | + vector for the embedding cache (whose key is derived from often-user-supplied |
| 71 | + text). It also emits a one-time warning when handed a Marshal-serializing |
| 72 | + store and recommends `value_serializer: nil`. |
| 73 | +- **CHANGED**: Documentation for Redis-backed caches, the embedding cache, and |
| 74 | + the synchronize-create lock store (`Parse.synchronize_create_store`) now |
| 75 | + builds the Redis store via `Parse::Cache::Redis` or `value_serializer: nil` |
| 76 | + so a raw `Moneta.new(:Redis, ...)` no longer leaves Marshal on the read path. |
| 77 | + |
| 78 | +#### Internal columns stripped from joined documents on mongo-direct reads |
| 79 | + |
| 80 | +- **FIXED**: `Parse::MongoDB.aggregate` now recursively strips Parse-internal |
| 81 | + credential columns (`_hashed_password`, `_session_token`, `_auth_data_*`, |
| 82 | + `_rperm`/`_wperm`, ...) from every result row **and every embedded |
| 83 | + sub-document** for scoped (non-master) callers. Previously a scoped caller |
| 84 | + could embed a foreign class (e.g. `_User` or `_Session`) into an arbitrary |
| 85 | + alias via `$lookup` / `$graphLookup` / `$unionWith` and read back password |
| 86 | + hashes, OAuth tokens, and session tokens: the per-class `protectedFields` |
| 87 | + strip is keyed on the outer class, and the ACL sub-document walk only drops |
| 88 | + ACL-failing sub-documents, so neither covered the aliased foreign document. |
| 89 | + A new `Parse::PipelineSecurity.redact_internal_fields_deep!` runs as the final |
| 90 | + redaction step. Structural columns (`_id`, `_p_*`, `_acl`, timestamps) are |
| 91 | + preserved, so object and ACL reconstruction are unaffected; master-key reads |
| 92 | + are unchanged. |
| 93 | + |
| 94 | +#### Hardened developer-facing mongo-direct aggregation terminals |
| 95 | + |
| 96 | +- **FIXED**: Credential columns (`_hashed_password`, `_session_token`, |
| 97 | + `_auth_data_*`, `_email_verify_token`, `_perishable_token`, ...) used as a |
| 98 | + `$match` field name are now refused **unconditionally** on the mongo-direct |
| 99 | + path — even on a pipeline running with `allow_internal_fields: true` (the flag |
| 100 | + that lets SDK-emitted `_rperm`/`_wperm` references through for |
| 101 | + `readable_by_role` / `publicly_readable`). Previously the `*_direct` terminals |
| 102 | + (`count_direct`, `results_direct`, `distinct_direct`, the direct group-by |
| 103 | + helpers) passed `allow_internal_fields: true` unconditionally, so a query |
| 104 | + whose `where` referenced a credential column compiled into a `$match` key that |
| 105 | + bypassed the internal-field screen — a count/match oracle that could bisect a |
| 106 | + bcrypt hash or session token. The ACL columns (`_rperm`/`_wperm`/`_tombstone`) |
| 107 | + remain gated by `allow_internal_fields`, so `readable_by_role` still works. |
| 108 | +- **FIXED**: `Parse::Query#aggregate` and `#aggregate_from_query` now treat a |
| 109 | + scoped query (`session_token` / `scope_to_user` / `scope_to_role`) as |
| 110 | + authoritative over an explicit `mongo_direct: false`. Previously passing |
| 111 | + `mongo_direct: false` on a scoped aggregation skipped the fail-closed guard |
| 112 | + and routed to Parse Server's master-key-only REST `/aggregate` endpoint, |
| 113 | + running the aggregation unscoped (no ACL, CLP, or `protectedFields`). A scoped |
| 114 | + aggregation now promotes to mongo-direct, or fails closed with |
| 115 | + `Parse::Query::MongoDirectRequired` when direct Mongo is unavailable; unscoped |
| 116 | + callers can still opt out to REST with `mongo_direct: false`. |
| 117 | + |
| 118 | +#### Additional hardening |
| 119 | + |
| 120 | +- **FIXED**: Request/response body logging now redacts credentials. At `:debug` |
| 121 | + level the logging middleware emitted login/signup request bodies (cleartext |
| 122 | + `password`) and auth response bodies (`sessionToken`, `authData`, MFA |
| 123 | + secrets); the body path now runs through the same `BodyBuilder.redact` |
| 124 | + scrubber the header path already used, before truncation. |
| 125 | +- **FIXED**: The `_User` REST endpoints (`fetch_user` / `update_user` / |
| 126 | + `delete_user`) now validate the `objectId` against |
| 127 | + `Parse::API::PathSegment.object_id!` before interpolating it into the path, |
| 128 | + matching the object endpoints. A crafted objectId (e.g. from a compromised |
| 129 | + server response) can no longer traverse to a different endpoint on a |
| 130 | + subsequent request. |
| 131 | +- **CHANGED**: `$sessionToken` / `$session_token` (the camelCase forms of the |
| 132 | + session-token column) are now in `DENIED_FIELD_REFS`, so they cannot be |
| 133 | + laundered through a `$`-field reference in a pipeline. |
| 134 | +- **IMPROVED**: The internal-collection floor (`_SCHEMA` / `_Hooks` / |
| 135 | + `_GlobalConfig` / `_Audit` / ...) is now enforced unconditionally on every |
| 136 | + `$lookup` / `$graphLookup` / `$unionWith` join target in |
| 137 | + `Parse::ACLScope`, not only when lookup-rewriting runs. This closes a |
| 138 | + defense-in-depth gap where an internal class whose CLP lookup returned no |
| 139 | + policy could otherwise have been joinable on the direct path. |
| 140 | +- **IMPROVED**: When the MCP agent server is started on an unauthenticated |
| 141 | + loopback bind with no Origin/custom-header gate configured, it now defaults |
| 142 | + to a loopback-only Origin policy. A browser DNS-rebinding attack against |
| 143 | + `127.0.0.1` carries a non-loopback `Origin` and is refused; native clients |
| 144 | + (which send no `Origin`) and local browser UIs are unaffected. A one-time |
| 145 | + warning points operators at `MCP_API_KEY` / `allowed_origins:` / |
| 146 | + `require_custom_header:` for routable deployments. |
| 147 | + |
3 | 148 | ### 5.5.0 |
4 | 149 |
|
5 | 150 | #### Multimodal bytes-fetch path with magic-byte MIME verification |
|
0 commit comments