Skip to content

v5.5.1 - Security hardening: ACL scoping, cache, mass-assignment#18

Merged
AdrianCurtin merged 1 commit into
mainfrom
v551
Jun 10, 2026
Merged

v5.5.1 - Security hardening: ACL scoping, cache, mass-assignment#18
AdrianCurtin merged 1 commit into
mainfrom
v551

Conversation

@AdrianCurtin

@AdrianCurtin AdrianCurtin commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

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_session are now scoped to
    the 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 / $geoIntersects query) previously ignored the fiber-local
    with_session token and fell through to a master-key read with no ACL/CLP
    enforcement. The resolver now mirrors Parse::Client#request precedence
    (explicit per-query token, then ambient session, then master); an explicit
    use_master_key: true still skips the ambient. Non-master clients
    (Parse.client_mode / user-scoped) route scoped rather than raising.

  • FIXED: Parse::MongoDB.aggregate now recursively strips Parse-internal
    credential columns (_hashed_password, _session_token, _auth_data_*,
    _rperm/_wperm, ...) from every result row and every embedded
    sub-document for scoped callers, closing a $lookup / $graphLookup /
    $unionWith path that could read back password hashes, OAuth tokens, and
    session tokens from an aliased foreign class.

  • FIXED: Credential columns used as a $match field name are now refused
    unconditionally on the developer-facing mongo-direct aggregation terminals
    (count_direct, results_direct, distinct_direct, the direct group-by
    paths), even on a pipeline running with allow_internal_fields: true.

  • FIXED: Parse::Cache::Redis and the embedding cache
    (Parse::Embeddings::Cache::MonetaStore) now serialize cached values as JSON
    instead of relying on Moneta's default Marshal serializer, removing a
    Marshal.load-on-read deserialization vector against a shared,
    unauthenticated, or plaintext-redis:// cache. Undecodable or legacy values
    are treated as a cache miss. The cache: "redis://..." shorthand now builds
    a Parse::Cache::Redis store so it inherits the same protection.

  • FIXED: Request/response body logging now redacts credentials. At
    :debug level the logging middleware previously emitted login/signup request
    bodies (cleartext password) and auth response bodies (sessionToken,
    authData, MFA secrets); the body path now runs through the same
    BodyBuilder.redact scrubber the header path already used.

  • FIXED: The _User REST endpoints (fetch_user / update_user /
    delete_user) now validate the objectId against
    Parse::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 :boolean property assigned a string now coerces via
    ActiveModel's boolean caster, so "false"/"0"/"off" map to false, a
    blank string is treated as unset (nil), and native booleans from Parse wire
    JSON pass through unchanged.

  • CHANGED: $sessionToken / $session_token (the camelCase forms of the
    session-token column) are now in DENIED_FIELD_REFS, so they cannot be
    laundered through a $-field reference in a pipeline. The caching middleware
    also 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 / $unionWith join target in Parse::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.1 while leaving native clients and local browser UIs
    unaffected. A one-time warning points operators at MCP_API_KEY /
    allowed_origins: / require_custom_header: for routable deployments.

  • DEPRECATED: Setting acl/ACL through 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 = params without StrongParameters — can grant
    unintended access. The behavior is unchanged this release (the ACL is still
    applied), the supported path is the explicit record.acl = ... setter, and a
    future release may block ACL mass-assignment. The constructor form
    Klass.new(acl: ...) is unaffected and does not warn.

Copilot AI review requested due to automatic review settings June 10, 2026 16:29

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 in Parse::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 thread lib/parse/cache/redis.rb
Comment on lines +256 to +260
def decode_value(raw)
return nil if raw.nil?
JSON.parse(raw)
rescue JSON::ParserError, EncodingError
nil
@AdrianCurtin AdrianCurtin force-pushed the v551 branch 2 times, most recently from a3cf7f1 to 3bdc105 Compare June 10, 2026 18:55
Comment thread lib/parse/query.rb Fixed
Comment thread test/lib/parse/mass_assignment_protection_test.rb Fixed
@AdrianCurtin AdrianCurtin changed the title Serialize Redis cache values as JSON v5.5.1 - Security hardening: ACL scoping, cache, mass-assignment Jun 10, 2026
@AdrianCurtin AdrianCurtin requested a review from Copilot June 10, 2026 19:19

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 30 out of 31 changed files in this pull request and generated 1 comment.

Comment thread lib/parse/cache/redis.rb
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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 30 out of 31 changed files in this pull request and generated 2 comments.

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 thread lib/parse/model/core/properties.rb Outdated
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.
@AdrianCurtin AdrianCurtin merged commit a68b53d into main Jun 10, 2026
11 checks passed
@AdrianCurtin AdrianCurtin deleted the v551 branch June 10, 2026 21:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants