Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions BREAKING_CHANGES_FOR_V16.md
Original file line number Diff line number Diff line change
@@ -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
```
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
25 changes: 9 additions & 16 deletions lib/shopify_api/auth/session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down