From 48835a032ac84b057599f0183702ec6a97ebf7e2 Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Mon, 18 Nov 2024 15:36:29 -0700 Subject: [PATCH] make sure we don't leak internal state via as_document (#5899) --- lib/mongoid/document.rb | 9 ++++++++- spec/mongoid/document_spec.rb | 27 +++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/lib/mongoid/document.rb b/lib/mongoid/document.rb index 3c30ccfed2..a388457467 100644 --- a/lib/mongoid/document.rb +++ b/lib/mongoid/document.rb @@ -133,7 +133,14 @@ def to_key # # @return [ Hash ] A hash of all attributes in the hierarchy. def as_document - BSON::Document.new(as_attributes) + attrs = as_attributes + + # legacy attributes have a tendency to leak internal state via + # `as_document`; we have to deep_dup the attributes here to prevent + # that. + attrs = attrs.deep_dup if Mongoid.legacy_attributes + + BSON::Document.new(attrs) end # Calls #as_json on the document with additional, Mongoid-specific options. diff --git a/spec/mongoid/document_spec.rb b/spec/mongoid/document_spec.rb index 447819a9e6..cd30d7593d 100644 --- a/spec/mongoid/document_spec.rb +++ b/spec/mongoid/document_spec.rb @@ -591,6 +591,33 @@ class << self; attr_accessor :name; end expect(person.as_document["addresses"].first).to have_key(:locations) end + context 'when modifying the returned object' do + let(:record) do + RootCategory.create(categories: [{ name: 'tests' }]).reload + end + + shared_examples_for 'an object with protected internal state' do + it 'does not expose internal state' do + before_change = record.as_document.dup + record.categories.first.name = 'things' + after_change = record.as_document + expect(before_change['categories'].first['name']).not_to eq('things') + end + end + + context 'when legacy_attributes is true' do + config_override :legacy_attributes, true + + it_behaves_like 'an object with protected internal state' + end + + context 'when legacy_attributes is false' do + config_override :legacy_attributes, false + + it_behaves_like 'an object with protected internal state' + end + end + context "with relation define store_as option in embeded_many" do let!(:phone) do