From d23b611112e3dee85a41432db25c349cc2a942d0 Mon Sep 17 00:00:00 2001 From: Liz Kenyon Date: Fri, 5 Dec 2025 10:37:39 -0600 Subject: [PATCH] Remove Session serialize/deserialize methods to fix RCE vulnerability The Session.deserialize method used Oj.load without safe mode, which allows instantiation of arbitrary Ruby objects. If an attacker could control session storage (e.g., compromise a Redis instance or database), they could inject malicious serialized data to achieve remote code execution. These methods were vestigial code from when the library handled session storage (deprecated in v12.3.0). After that deprecation, apps became responsible for their own session persistence, rendering serialize/deserialize unnecessary for their original purpose. Investigation confirmed no external usage - the shopify_app gem stores individual session attributes in database columns and reconstructs sessions using Session.new(). The only internal usage was copy_attributes_from, which called serialize just to enumerate attribute names via JSON.parse(other.serialize).keys before copying instance variables. This has been refactored to directly copy each attribute, eliminating the dependency on serialize. Breaking change: Session#serialize and Session.deserialize removed. Migration: Apps should use Session.new() to reconstruct sessions from stored attributes (the pattern already used by shopify_app). Complete removal eliminates the RCE vector entirely while maintaining all functionality. --- BREAKING_CHANGES_FOR_V16.md | 63 +++++++++++++++++++++++++++++++++ CHANGELOG.md | 1 + lib/shopify_api/auth/session.rb | 25 +++++-------- 3 files changed, 73 insertions(+), 16 deletions(-) create mode 100644 BREAKING_CHANGES_FOR_V16.md diff --git a/BREAKING_CHANGES_FOR_V16.md b/BREAKING_CHANGES_FOR_V16.md new file mode 100644 index 000000000..3259869c5 --- /dev/null +++ b/BREAKING_CHANGES_FOR_V16.md @@ -0,0 +1,63 @@ +# Breaking change notice for version 16.0.0 + +## Removal of `Session#serialize` and `Session.deserialize` methods + +The `Session#serialize` and `Session.deserialize` methods have been removed due to a security vulnerability. The `deserialize` method used `Oj.load` without safe mode, which allows instantiation of arbitrary Ruby objects. + +These methods were originally created for session persistence when the library handled session storage. After session storage was deprecated in v12.3.0, applications became responsible for their own session persistence, making these methods unnecessary for their original purpose. + +### Why this change? + +**No impact on most applications:** The `shopify_app gem` stores individual session attributes in database columns and reconstructs sessions using `Session.new()`, which is the recommended pattern. + +## Migration Guide + +If your application was using `Session#serialize` and `Session.deserialize` for session persistence, you'll need to refactor to store individual session attributes and reconstruct sessions using `Session.new()`. + +### Previous implementation (removed in v16.0.0) + +```ruby +# Storing a session +session = ShopifyAPI::Auth::Session.new( + shop: "example.myshopify.com", + access_token: "shpat_xxxxx", + scope: "read_products,write_orders" +) + +serialized_data = session.serialize +# Store serialized_data in Redis, database, etc. +redis.set("session:#{session.id}", serialized_data) + +# Retrieving a session +serialized_data = redis.get("session:#{session_id}") +session = ShopifyAPI::Auth::Session.deserialize(serialized_data) +``` + +### New implementation (required in v16.0.0) + +Store individual session attributes and reconstruct using `Session.new()`: + +## Reference: shopify_app gem implementation + +The [shopify_app gem](https://github.com/Shopify/shopify_app) provides a reference implementation of session storage that follows these best practices: + +**Shop Session Storage** ([source](https://github.com/Shopify/shopify_app/blob/main/lib/shopify_app/session/shop_session_storage.rb)): +```ruby +# Stores attributes in database columns +def store(auth_session) + shop = find_or_initialize_by(shopify_domain: auth_session.shop) + shop.shopify_token = auth_session.access_token + shop.save! +end + +# Reconstructs using Session.new() +def retrieve(id) + shop = find_by(id: id) + return unless shop + + ShopifyAPI::Auth::Session.new( + shop: shop.shopify_domain, + access_token: shop.shopify_token + ) +end +``` diff --git a/CHANGELOG.md b/CHANGELOG.md index 166152d1e..3e8e88f91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ Note: For changes to the API, see https://shopify.dev/changelog?filter=api ## Unreleased +- ⚠️ [Breaking] Removed `Session#serialize` and `Session.deserialize` methods due to security concerns (RCE vulnerability via `Oj.load`). These methods were not used internally by the library. If your application relies on session serialization, use `Session.new()` to reconstruct sessions from stored attributes instead. ### 15.0.0 diff --git a/lib/shopify_api/auth/session.rb b/lib/shopify_api/auth/session.rb index 68cef2774..3642f4ded 100644 --- a/lib/shopify_api/auth/session.rb +++ b/lib/shopify_api/auth/session.rb @@ -117,29 +117,22 @@ def from(shop:, access_token_response:) shopify_session_id: access_token_response.session, ) end - - sig { params(str: String).returns(Session) } - def deserialize(str) - Oj.load(str) - end end sig { params(other: Session).returns(Session) } def copy_attributes_from(other) - JSON.parse(other.serialize).keys.each do |key| - next if key.include?("^") - - variable_name = "@#{key}" - instance_variable_set(variable_name, other.instance_variable_get(variable_name)) - end + @shop = other.shop + @state = other.state + @access_token = other.access_token + @scope = other.scope + @associated_user_scope = other.associated_user_scope + @expires = other.expires + @associated_user = other.associated_user + @is_online = other.online? + @shopify_session_id = other.shopify_session_id self end - sig { returns(String) } - def serialize - Oj.dump(self) - end - alias_method :eql?, :== sig { params(other: T.nilable(Session)).returns(T::Boolean) } def ==(other)