Rails serializes store data as a regular hash instead of using HWIA


ActiveRecord’s store provides a simple way to store hashes in a single column, regardless of database support. This functionality is enabled by serialization. A hash is converted either into to a string or a binary format before being stored. This action is known as “dumping”. When the data is read back from the database, it is loaded and converted to a hash. Serialization is a core Ruby module provided by the Marshal library.

Let’s create a simple Rails model which uses store.

   rails g model user name:string profile:text
    invoke  active_record
    create    db/migrate/20221111115257_create_users.rb
    create    app/models/user.rb
    invoke    test_unit
    create      test/models/user_test.rb
    create      test/fixtures/users.yml

The profile column, though a text column, is serialized as a hash. Let’s add a store accessor to the model. There are many more options available on the Rails guides.

  class User
    store :profile, accessors: [ :dnd, :theme ]
  end

Before

We can now create a User object and set the profile attributes.

  irb(main):001:0> user = User.create!(name: "Emily", dnd: true, theme: "dark")
  (1.8ms)  SELECT sqlite_version(*)
  TRANSACTION (0.0ms)  begin transaction 
  JsonObj Create (1.5ms)  INSERT INTO "users" ("profile", "created_at", "updated_at", "name") VALUES (?, ?, ?, ?)  [["profile", "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\dnd: true\n\theme: dark\n"], ["created_at", "2022-11-11 08:46:32.271095"], ["updated_at", "2022-11-11 08:46:32.271095"], ["name", "Emily"]]
  TRANSACTION (0.3ms)  commit transaction
  (Object doesn't support #inspect)        
  =>                                       
  irb(main):003:0> user.dnd
  /Users/swaathi/.rbenv/versions/3.1.2/lib/ruby/3.1.0/psych/class_loader.rb:99:in `find': Tried to load unspecified class: ActiveSupport::HashWithIndifferentAccess (Psych::DisallowedClass)                                 

There are two errors here,

  1. The profile column is serialized as a HashWithIndifferentAccess object which does not support the #inspect method.
  2. We are unable to access the dnd attribute.

This is due to a potential RCE in YAML serialized columns in the database.

When serialized columns that use YAML (the default) are deserialized, Rails uses YAML.unsafe_load to convert the YAML data in to Ruby objects. If an attacker can manipulate data in the database (via means like SQL injection), then it may be possible for the attacker to escalate to an RCE.

To safe guard against this, Rails changed the default YAML deserializer to use YAML.safe_load, which prevents deserialization of possibly dangerous objects. Unfortunately, this meant store which uses YAML serialization via HashWithIndifferentAccess was also affected.

After

The solution was for Rails to deserialize YAML data into a regular hash and then cast it back to HashWithIndifferentAccess before accessing. Thanks to this PR ActiveRecord store no longer uses HashWithIndifferentAccess and instead serializes data to a regular hash.

  irb(main):001:0> user = User.create!(name: "Emily", dnd: true, theme: "dark")
  TRANSACTION (0.1ms)  begin transaction
  User Create (1.5ms)  INSERT INTO "users" ("profile", "created_at", "updated_at", "name") VALUES (?, ?, ?, ?)  [["profile", "---\ndnd: true\ntheme: dark"], ["created_at", "2022-11-11 08:45:13.289388"], ["updated_at", "2022-11-11 08:45:13.289388"], ["name", "Emily"]]    
  TRANSACTION (0.4ms)  commit transaction                                                
  =>                                                                                     #<User:0x000000010fb86b08                                                             
  ...                                                                                      
  irb(main):003:0> user.dnd
  => true

Join Our Newsletter