v5.5.1 - Security hardening: ACL scoping, cache, mass-assignment#18
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
This PR hardens the Redis-backed caching layer by eliminating Marshal deserialization on cache hits (mitigating RCE risk if Redis is compromised) and migrating cached HTTP response values to JSON with backward-compatible miss-on-legacy behavior.
Changes:
- Force Moneta Redis value serialization off (
value_serializer: nil) and JSON-(de)serialize cached values inParse::Cache::Redis. - Update caching middleware to write cache entries using string keys (with symbol-key fallback on reads) to preserve fidelity through JSON.
- Add tests covering JSON encoding, serializer forcing, hostile Marshal bytes, and bump version/changelog to 5.5.1.
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
lib/parse/cache/redis.rb |
Disables Moneta value serializer and adds JSON encode/decode helpers for Redis cache values. |
lib/parse/client/caching.rb |
Stores cache payloads with string keys and adds backwards-compatible read fallback for legacy symbol keys. |
test/lib/parse/cache_redis_wrapper_test.rb |
Adds unit tests asserting JSON encoding and safe handling of Marshal/invalid blobs. |
test/lib/parse/cache_write_only_test.rb |
Updates expectations to read cached response body from string-keyed entries. |
lib/parse/stack/version.rb |
Bumps gem version to 5.5.1. |
Gemfile.lock |
Updates locked gem version to 5.5.1. |
CHANGELOG.md |
Documents the security hardening and behavioral changes in 5.5.1. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+256
to
+260
| def decode_value(raw) | ||
| return nil if raw.nil? | ||
| JSON.parse(raw) | ||
| rescue JSON::ParserError, EncodingError | ||
| nil |
a3cf7f1 to
3bdc105
Compare
Comment on lines
119
to
123
| def store(key, value, options = {}) | ||
| @pool.store(key, value, options) | ||
| @pool.store(key, encode_value(value), options) | ||
| end | ||
|
|
||
| # Atomic SETNX. Required so `Parse::CreateLock` can acquire |
Comment on lines
+675
to
678
| if origin_refused?(env) | ||
| @logger&.warn("[Parse::Agent::MCPRackApp] Origin refused: #{env["HTTP_ORIGIN"].to_s.strip.inspect}") | ||
| return [403, json_headers, [json_rpc_error(-32_700, "Origin not allowed")]] | ||
| end |
Comment on lines
+870
to
+878
| # Coerce via ActiveModel's boolean caster rather than Ruby | ||
| # truthiness. Plain `val ? true : false` treats every non-nil, | ||
| # non-false object as true, so the strings "false", "0", and "off" | ||
| # — exactly what arrives on the Rails-form / query-string ingestion | ||
| # path — would coerce to `true` and silently flip a boolean the | ||
| # wrong way (e.g. an `archived` or admin gate). ActiveModel maps the | ||
| # string forms ("false"/"0"/"f"/"off"/"") to false/nil. Parse wire | ||
| # JSON already sends real booleans, which pass through unchanged. | ||
| val = val.nil? ? nil : ActiveModel::Type::Boolean.new.cast(val) |
Multiple security hardenings and related fixes: switch Redis-backed caches to JSON serialization (disable Moneta value serializer) and update caching middleware / embeddings cache to round-trip safely; make Parse::Client cache shorthand build the Parse::Cache::Redis wrapper. Tighten mongo-direct pipeline safety by adding deep redaction of internal credential fields, a credential denylist for pipeline $match keys, and other aggregate-routing guards. Add objectId validation for REST user endpoints, fix boolean property coercion to use ActiveModel boolean casting, warn on ACL mass-assignment, and redact request/response bodies in logging. Add MCP agent loopback-origin default and origin checks to mitigate DNS-rebinding on unauthenticated loopback binds. Bump release to 5.5.1 and update CHANGELOG/README/docs and tests accordingly.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Security hardening release
This release closes a set of security boundaries across the mongo-direct
enforcement path, the model layer, the response cache, logging, and the agent
server. There are no breaking changes; one behavior is newly deprecated with a
warning.
Changes
FIXED: Mongo-direct reads inside
Parse.with_sessionare now scoped tothe ambient session instead of running as master. A query that auto-routes
to the mongo-direct path because of a direct-only constraint (e.g. a geo
$near/$geoIntersectsquery) previously ignored the fiber-localwith_sessiontoken and fell through to a master-key read with no ACL/CLPenforcement. The resolver now mirrors
Parse::Client#requestprecedence(explicit per-query token, then ambient session, then master); an explicit
use_master_key: truestill skips the ambient. Non-master clients(
Parse.client_mode/ user-scoped) route scoped rather than raising.FIXED:
Parse::MongoDB.aggregatenow recursively strips Parse-internalcredential columns (
_hashed_password,_session_token,_auth_data_*,_rperm/_wperm, ...) from every result row and every embeddedsub-document for scoped callers, closing a
$lookup/$graphLookup/$unionWithpath that could read back password hashes, OAuth tokens, andsession tokens from an aliased foreign class.
FIXED: Credential columns used as a
$matchfield name are now refusedunconditionally on the developer-facing mongo-direct aggregation terminals
(
count_direct,results_direct,distinct_direct, the direct group-bypaths), even on a pipeline running with
allow_internal_fields: true.FIXED:
Parse::Cache::Redisand the embedding cache(
Parse::Embeddings::Cache::MonetaStore) now serialize cached values as JSONinstead of relying on Moneta's default
Marshalserializer, removing aMarshal.load-on-read deserialization vector against a shared,unauthenticated, or plaintext-
redis://cache. Undecodable or legacy valuesare treated as a cache miss. The
cache: "redis://..."shorthand now buildsa
Parse::Cache::Redisstore so it inherits the same protection.FIXED: Request/response body logging now redacts credentials. At
:debuglevel the logging middleware previously emitted login/signup requestbodies (cleartext
password) and auth response bodies (sessionToken,authData, MFA secrets); the body path now runs through the sameBodyBuilder.redactscrubber the header path already used.FIXED: The
_UserREST endpoints (fetch_user/update_user/delete_user) now validate theobjectIdagainstParse::API::PathSegment.object_id!before interpolating it into the path,so a crafted objectId cannot traverse to a different endpoint.
FIXED: Boolean property coercion no longer treats the string
"false"as
true. A:booleanproperty assigned a string now coerces viaActiveModel's boolean caster, so
"false"/"0"/"off"map tofalse, ablank string is treated as unset (
nil), and native booleans from Parse wireJSON pass through unchanged.
CHANGED:
$sessionToken/$session_token(the camelCase forms of thesession-token column) are now in
DENIED_FIELD_REFS, so they cannot belaundered through a
$-field reference in a pipeline. The caching middlewarealso stores response entries with string keys so they round-trip losslessly
through JSON serialization.
IMPROVED: The internal-collection floor (
_SCHEMA/_Hooks/_GlobalConfig/_Audit/ ...) is now enforced unconditionally on every$lookup/$graphLookup/$unionWithjoin target inParse::ACLScope,not only when lookup-rewriting runs.
IMPROVED: When the MCP agent server is started on an unauthenticated
loopback bind with no Origin/custom-header gate configured, it now defaults
to a loopback-only Origin policy that refuses a browser DNS-rebinding attack
against
127.0.0.1while leaving native clients and local browser UIsunaffected. A one-time warning points operators at
MCP_API_KEY/allowed_origins:/require_custom_header:for routable deployments.DEPRECATED: Setting
acl/ACLthrough mass-assignment(
Parse::Object#attributes=) now emits a one-time security warning.Mass-assigning an ACL from a caller-supplied hash — for example a controller
doing
record.attributes = paramswithout StrongParameters — can grantunintended access. The behavior is unchanged this release (the ACL is still
applied), the supported path is the explicit
record.acl = ...setter, and afuture release may block ACL mass-assignment. The constructor form
Klass.new(acl: ...)is unaffected and does not warn.