Insecure Deserialization in Rails with Mongodb
Insecure Deserialization in Rails with Mongodb — how this specific combination creates or exposes the vulnerability
Insecure deserialization occurs when an application accepts and processes serialized data from untrusted sources. In a Ruby on Rails stack using MongoDB as the primary datastore, this typically involves the application deserializing user-controlled payloads (e.g., params, cookies, or HTTP bodies) into Ruby objects before storing or acting on them. MongoDB Ruby driver and Object Document Mapper (ODM) libraries like Mongoid can store complex Ruby types such as Date, Time, or custom classes; this flexibility becomes dangerous when deserialization of attacker-supplied data is used to instantiate objects.
For example, Rails params are often deserialized from JSON or form data. If the app passes raw params to a method that reconstructs Ruby objects (e.g., using YAML.safe_load without strict whitelisting or using libraries that invoke eval-like behavior), an attacker can craft payloads that execute code during deserialization. In a MongoDB context, if the application stores user input into documents and later reconstructs those documents using unsafe deserialization (e.g., BSON decoding that allows arbitrary class instantiation), the attacker may achieve remote code execution (RCE) or tamper with server-side logic. Common patterns include storing serialized objects in a field and then loading them back with YAML.load or similar unsafe methods, bypassing Rails’ permitted parameters and model validations.
Real-world attack patterns mirror known CVEs and OWASP API Top 10 risks: injection of malicious class definitions that execute code when the object is instantiated or when callbacks (e.g., after_find) run. Because MongoDB stores rich document structures, attackers can embed serialized Ruby objects or use type confusion to escalate privileges, bypass authentication checks, or achieve persistence. The risk is compounded when endpoints that expose deserialization functionality do not enforce strict input validation, rely on default BSON decoders that accept Ruby-type hints, or allow client-supplied class names in JSON/MessagePack payloads.
Mongodb-Specific Remediation in Rails — concrete code fixes
Apply strict deserialization controls and avoid unsafe methods when working with MongoDB in Rails. Prefer JSON parsing over YAML, and never pass raw user input to deserialization routines that can instantiate arbitrary Ruby classes.
1. Use safe JSON parsing and strong parameters
Rails’ built-in JSON parser does not invoke arbitrary class deserialization. Ensure your controllers use strong parameters and avoid custom deserializers that rely on YAML or Marshal.
class ProfilesController < ApplicationController
def create
# Safe: JSON parsing produces plain hashes/arrays, no arbitrary class instantiation
data = JSON.parse(params[:profile_data], symbolize_names: true)
profile = current_user.profiles.new(data.slice(:bio, :preferences))
if profile.save
render json: profile, status: :created
else
render json: { errors: profile.errors }, status: :unprocessable_entity
end
end
end
2. Avoid unsafe YAML.load; use safe_load with explicit classes
If you must deserialize YAML (rare in typical Rails APIs), always use YAML.safe_load with a whitelist of permitted classes and never allow user input to specify class names.
# config/initializers/yaml.rb
YAML_ALLOWED_CLASSES = [Date, Time, TrueClass, FalseClass, NilClass, String, Integer, Float, Array, Hash]
# In a service object that processes stored BSON/YAML fields
class DeserializeProfile
def self.call(serialized)
# Only permit specific classes; reject arbitrary Ruby objects
YAML.safe_load(serialized, permitted_classes: YAML_ALLOWED_CLASSES)
end
end
# Usage in a model or controller
profile_data = DeserializeProfile.call(params[:legacy_payload])
3. Configure MongoDB/ODM to reject Ruby-type hints
When using Mongoid, disable loading of Ruby-specific type hints (e.g., _type) that enable arbitrary class instantiation from BSON. Validate and cast fields explicitly instead of relying on automatic deserialization.
# config/mongoid.yml
production:
options:
# Avoid automatic type casting that may instantiate unexpected classes
allow_dynamic_fields: false
# In a Mongoid model, prefer explicit casting and validation
class PaymentLog
include Mongoid::Document
field :event_type, type: String
field :metadata, type: Hash, default: {}
# Custom setter to reject unexpected class keys
def metadata=(value)
raise ArgumentError, "metadata must be a plain hash" unless value.is_a?(Hash)
super(value.stringify_keys)
end
end
# Safe retrieval without invoking eval-like behavior
raw = collection.find(filter).first
# Parse BSON carefully; do not allow raw Ruby object reconstruction
plain = { event_type: raw["event_type"], metadata: raw["metadata"] }
4. Validate and sanitize before persistence
Treat any field that may have been deserialized as hostile. Normalize and validate before saving to MongoDB. For example, if your API accepts JSON that could be transformed into a serialized Ruby object, ensure the schema and permitted keys are strictly enforced.
class Api::V1::DocumentsController < ApplicationController
def create
doc_params = params.require(:document).permit(:title, :body, tags: [])
# No YAML/Marshal loading; store only whitelisted fields
doc = current_user.documents.create!(doc_params)
render json: doc, status: :created
end
end
5. Use BSON decoding options that restrict class instantiation
When interacting with MongoDB drivers directly, configure BSON to avoid loading Ruby-specific type metadata that can trigger unsafe deserialization.
# Safe BSON decoding in a service object
require "bson"
raw_bson = params[:bson_payload] # raw binary or base64 as needed
# Decode without allowing arbitrary Ruby class instantiation
decoder = BSON::Decoder.new(allow_array: true, allow_hash: true)
# Do NOT pass allow_ruby: true
plain = decoder.decode(raw_bson)
# Use plain hashes/arrays only; map to models explicitly
attrs = plain.slice("title", "count")
Record.create!(attrs)