Skip to content

Commit d50649d

Browse files
[Release 4.6.0] Bumping version of httparty to ensure protection from CVE-2025-68696 (#100)
* Bump the minimum version of httparty to 0.23.3 to ensure protection against CVE-2025-68696 * Refactor Client to use dedicated internal HTTP clients for different API endpoints * Fix Verification API methods to use correct version parameter * Increase minimum required version for Ruby to 2.7.0 * Release 4.6.0
1 parent 9042213 commit d50649d

13 files changed

Lines changed: 202 additions & 56 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
runs-on: ubuntu-latest
2121
strategy:
2222
matrix:
23-
ruby-version: ['2.4.2']
23+
ruby-version: ['2.7', '3.1', '3.2', '3.3']
2424
steps:
2525
- uses: actions/checkout@v4
2626
- name: Set up Ruby
@@ -36,7 +36,7 @@ jobs:
3636
if: ${{ github.ref == 'refs/heads/master' }}
3737
strategy:
3838
matrix:
39-
ruby-version: [ '2.4.2' ]
39+
ruby-version: ['2.7', '3.1', '3.2', '3.3']
4040
steps:
4141
- uses: actions/checkout@v4
4242
- name: Set up Ruby

HISTORY

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
=== 4.6.0 2026-02-10
2+
- Bump the minimum version of httparty to 0.23.3 to ensure protection against CVE-2025-68696
3+
- Refactor Client to use dedicated internal HTTP clients for different API endpoints
4+
- Fix Verification API methods to use correct version parameter
5+
- Increase minimum required version for Ruby to 2.7.0
6+
17
=== 4.5.1 2025-04-07
28
- Fix Verification URLs
39

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ The official Ruby bindings for the latest version (v205) of the [Sift API](https
44

55
## Requirements
66

7-
* Ruby 2.0.0 or above.
7+
* Ruby 2.7.0 or above.
88

99

1010
## Installation

lib/sift/client.rb

Lines changed: 49 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22
require "multi_json"
33
require "base64"
44

5-
require_relative "./client/decision"
6-
require_relative "./error"
7-
85
module Sift
96

107
# Represents the payload returned from a call through the track API
@@ -94,17 +91,36 @@ class Client
9491
API_ENDPOINT = ENV["SIFT_RUBY_API_URL"] || 'https://api.siftscience.com'
9592
API3_ENDPOINT = ENV["SIFT_RUBY_API3_URL"] || 'https://api3.siftscience.com'
9693

94+
# Maintain backward compatibility for users who may rely on HTTParty methods
9795
include HTTParty
9896
base_uri API_ENDPOINT
9997

10098
attr_reader :api_key, :account_id
10199

102-
def self.build_auth_header(api_key)
103-
{ "Authorization" => "Basic #{Base64.encode64(api_key)}" }
104-
end
100+
class << self
101+
def build_auth_header(api_key)
102+
{ "Authorization" => "Basic #{Base64.strict_encode64(api_key + ":")}" }
103+
end
104+
105+
def user_agent
106+
"sift-ruby/#{VERSION}"
107+
end
108+
109+
# Factory methods for internal API executors that inherit from the current class context.
110+
# This ensures that subclasses of Client propagate their HTTParty configuration
111+
# to these internal clients.
105112

106-
def self.user_agent
107-
"sift-ruby/#{VERSION}"
113+
def api_client
114+
@api_client ||= Class.new(self) do
115+
base_uri API_ENDPOINT
116+
end
117+
end
118+
119+
def api3_client
120+
@api3_client ||= Class.new(self) do
121+
base_uri API3_ENDPOINT
122+
end
123+
end
108124
end
109125

110126
# Constructor
@@ -256,7 +272,7 @@ def track(event, properties = {}, opts = {})
256272
}
257273
options.merge!(:timeout => timeout) unless timeout.nil?
258274

259-
response = self.class.post(path, options)
275+
response = self.class.api_client.post(path, options)
260276
Response.new(response.body, response.code, response.response)
261277
end
262278

@@ -319,7 +335,7 @@ def score(user_id, opts = {})
319335
}
320336
options.merge!(:timeout => timeout) unless timeout.nil?
321337

322-
response = self.class.get(Sift.score_api_path(user_id, version), options)
338+
response = self.class.api_client.get(Sift.score_api_path(user_id, version), options)
323339
Response.new(response.body, response.code, response.response)
324340
end
325341

@@ -364,6 +380,7 @@ def get_user_score(user_id, opts = {})
364380
abuse_types = opts[:abuse_types]
365381
api_key = opts[:api_key] || @api_key
366382
timeout = opts[:timeout] || @timeout
383+
version = opts[:version] || @version
367384
include_score_percentiles = opts[:include_score_percentiles]
368385

369386
raise("user_id must be a non-empty string") if (!user_id.is_a? String) || user_id.to_s.empty?
@@ -382,7 +399,7 @@ def get_user_score(user_id, opts = {})
382399
}
383400
options.merge!(:timeout => timeout) unless timeout.nil?
384401

385-
response = self.class.get(Sift.user_score_api_path(user_id, @version), options)
402+
response = self.class.api_client.get(Sift.user_score_api_path(user_id, version), options)
386403
Response.new(response.body, response.code, response.response)
387404
end
388405

@@ -420,6 +437,7 @@ def rescore_user(user_id, opts = {})
420437
abuse_types = opts[:abuse_types]
421438
api_key = opts[:api_key] || @api_key
422439
timeout = opts[:timeout] || @timeout
440+
version = opts[:version] || @version
423441

424442
raise("user_id must be a non-empty string") if (!user_id.is_a? String) || user_id.to_s.empty?
425443
raise("Bad api_key parameter") if api_key.empty?
@@ -434,7 +452,7 @@ def rescore_user(user_id, opts = {})
434452
}
435453
options.merge!(:timeout => timeout) unless timeout.nil?
436454

437-
response = self.class.post(Sift.user_score_api_path(user_id, @version), options)
455+
response = self.class.api_client.post(Sift.user_score_api_path(user_id, version), options)
438456
Response.new(response.body, response.code, response.response)
439457
end
440458

@@ -532,7 +550,7 @@ def unlabel(user_id, opts = {})
532550
}
533551
options.merge!(:timeout => timeout) unless timeout.nil?
534552

535-
response = self.class.delete(Sift.users_label_api_path(user_id, version), options)
553+
response = self.class.api_client.delete(Sift.users_label_api_path(user_id, version), options)
536554
Response.new(response.body, response.code, response.response)
537555
end
538556

@@ -569,8 +587,7 @@ def get_workflow_status(run_id, opts = {})
569587
}
570588
options.merge!(:timeout => timeout) unless timeout.nil?
571589

572-
uri = API3_ENDPOINT + Sift.workflow_status_path(account_id, run_id)
573-
response = self.class.get(uri, options)
590+
response = self.class.api3_client.get(Sift.workflow_status_path(account_id, run_id), options)
574591
Response.new(response.body, response.code, response.response)
575592
end
576593

@@ -607,8 +624,7 @@ def get_user_decisions(user_id, opts = {})
607624
}
608625
options.merge!(:timeout => timeout) unless timeout.nil?
609626

610-
uri = API3_ENDPOINT + Sift.user_decisions_api_path(account_id, user_id)
611-
response = self.class.get(uri, options)
627+
response = self.class.api3_client.get(Sift.user_decisions_api_path(account_id, user_id), options)
612628
Response.new(response.body, response.code, response.response)
613629
end
614630

@@ -645,8 +661,7 @@ def get_order_decisions(order_id, opts = {})
645661
}
646662
options.merge!(:timeout => timeout) unless timeout.nil?
647663

648-
uri = API3_ENDPOINT + Sift.order_decisions_api_path(account_id, order_id)
649-
response = self.class.get(uri, options)
664+
response = self.class.api3_client.get(Sift.order_decisions_api_path(account_id, order_id), options)
650665
Response.new(response.body, response.code, response.response)
651666
end
652667

@@ -685,8 +700,7 @@ def get_session_decisions(user_id, session_id, opts = {})
685700
}
686701
options.merge!(:timeout => timeout) unless timeout.nil?
687702

688-
uri = API3_ENDPOINT + Sift.session_decisions_api_path(account_id, user_id, session_id)
689-
response = self.class.get(uri, options)
703+
response = self.class.api3_client.get(Sift.session_decisions_api_path(account_id, user_id, session_id), options)
690704
Response.new(response.body, response.code, response.response)
691705
end
692706

@@ -725,8 +739,7 @@ def get_content_decisions(user_id, content_id, opts = {})
725739
}
726740
options.merge!(:timeout => timeout) unless timeout.nil?
727741

728-
uri = API3_ENDPOINT + Sift.content_decisions_api_path(account_id, user_id, content_id)
729-
response = self.class.get(uri, options)
742+
response = self.class.api3_client.get(Sift.content_decisions_api_path(account_id, user_id, content_id), options)
730743
Response.new(response.body, response.code, response.response)
731744
end
732745

@@ -748,7 +761,7 @@ def apply_decision!(configs = {})
748761

749762
def build_default_headers_post(api_key)
750763
{
751-
"Authorization" => "Basic #{Base64.encode64(api_key+":")}",
764+
"Authorization" => "Basic #{Base64.strict_encode64(api_key+":")}",
752765
"User-Agent" => "SiftScience/v#{@version} sift-ruby/#{VERSION}",
753766
"Content-Type" => "application/json"
754767
}
@@ -768,7 +781,7 @@ def verification_send(properties = {}, opts = {})
768781
:headers => build_default_headers_post(api_key)
769782
}
770783
options.merge!(:timeout => timeout) unless timeout.nil?
771-
response = self.class.post(Sift.verification_api_send_path(@version), options)
784+
response = self.class.api_client.post(Sift.verification_api_send_path(version), options)
772785
Response.new(response.body, response.code, response.response)
773786
end
774787

@@ -787,7 +800,7 @@ def verification_resend(properties = {}, opts = {})
787800
}
788801
options.merge!(:timeout => timeout) unless timeout.nil?
789802

790-
response = self.class.post(Sift.verification_api_resend_path(@version), options)
803+
response = self.class.api_client.post(Sift.verification_api_resend_path(version), options)
791804
Response.new(response.body, response.code, response.response)
792805
end
793806

@@ -806,7 +819,7 @@ def verification_check(properties = {}, opts = {})
806819
}
807820
options.merge!(:timeout => timeout) unless timeout.nil?
808821

809-
response = self.class.post(Sift.verification_api_check_path(@version), options)
822+
response = self.class.api_client.post(Sift.verification_api_check_path(version), options)
810823
Response.new(response.body, response.code, response.response)
811824
end
812825

@@ -831,7 +844,7 @@ def create_psp_merchant_profile(properties = {}, opts = {})
831844
:basic_auth => { :username => api_key, :password => "" }
832845
}
833846
options.merge!(:timeout => timeout) unless timeout.nil?
834-
response = self.class.post(API_ENDPOINT + Sift.psp_merchant_api_path(account_id), options)
847+
response = self.class.api_client.post(Sift.psp_merchant_api_path(account_id), options)
835848
Response.new(response.body, response.code, response.response)
836849
end
837850

@@ -858,7 +871,7 @@ def update_psp_merchant_profile(merchant_id, properties = {}, opts = {})
858871
:basic_auth => { :username => api_key, :password => "" }
859872
}
860873
options.merge!(:timeout => timeout) unless timeout.nil?
861-
response = self.class.put(API_ENDPOINT + Sift.psp_merchant_id_api_path(account_id, merchant_id), options)
874+
response = self.class.api_client.put(Sift.psp_merchant_id_api_path(account_id, merchant_id), options)
862875
Response.new(response.body, response.code, response.response)
863876
end
864877

@@ -882,7 +895,7 @@ def get_a_psp_merchant_profile(merchant_id, opts = {})
882895
:basic_auth => { :username => api_key, :password => "" }
883896
}
884897
options.merge!(:timeout => timeout) unless timeout.nil?
885-
response = self.class.get(API_ENDPOINT + Sift.psp_merchant_id_api_path(account_id, merchant_id), options)
898+
response = self.class.api_client.get(Sift.psp_merchant_id_api_path(account_id, merchant_id), options)
886899
Response.new(response.body, response.code, response.response)
887900
end
888901

@@ -911,7 +924,7 @@ def get_psp_merchant_profiles(batch_size = nil, batch_token = nil, opts = {})
911924
:query => query
912925
}
913926
options.merge!(:timeout => timeout) unless timeout.nil?
914-
response = self.class.get(API_ENDPOINT + Sift.psp_merchant_api_path(account_id), options)
927+
response = self.class.api_client.get(Sift.psp_merchant_api_path(account_id), options)
915928
Response.new(response.body, response.code, response.response)
916929
end
917930

@@ -926,7 +939,7 @@ def handle_response(response)
926939
end
927940

928941
def decision_instance
929-
@decision_instance ||= Decision.new(api_key, account_id)
942+
@decision_instance ||= Decision.new(api_key, account_id, self.class)
930943
end
931944

932945
def delete_nils(properties)
@@ -943,4 +956,8 @@ def delete_nils(properties)
943956
end
944957
end
945958
end
959+
960+
require_relative "./client/decision"
961+
require_relative "./error"
962+
946963
end

lib/sift/client/decision.rb

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@ class Client
1010
class Decision
1111
FILTER_PARAMS = %w{ limit entity_type abuse_types from }
1212

13-
attr_reader :account_id, :api_key
13+
attr_reader :account_id, :api_key, :client_class
1414

15-
def initialize(api_key, account_id)
15+
def initialize(api_key, account_id, client_class = Sift::Client)
1616
@account_id = account_id
1717
@api_key = api_key
18+
@client_class = client_class
1819
end
1920

2021
def list(options = {})
@@ -25,7 +26,8 @@ def list(options = {})
2526
else
2627
Router.get(index_path, {
2728
query: build_query(getter),
28-
headers: auth_header
29+
headers: auth_header,
30+
client_class: client_class
2931
})
3032
end
3133
end
@@ -44,7 +46,7 @@ def apply_to(configs = {})
4446
getter = Utils::HashGetter.new(configs)
4547
configs[:account_id] = account_id
4648

47-
ApplyTo.new(api_key, getter.get(:decision_id), configs).run
49+
ApplyTo.new(api_key, getter.get(:decision_id), configs, client_class).run
4850
end
4951

5052
def index_path
@@ -54,7 +56,7 @@ def index_path
5456
private
5557

5658
def request_next_page(path)
57-
Router.get(path, headers: auth_header)
59+
Router.get(path, headers: auth_header, client_class: client_class)
5860
end
5961

6062
def auth_header
@@ -63,4 +65,3 @@ def auth_header
6365
end
6466
end
6567
end
66-

lib/sift/client/decision/apply_to.rb

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class ApplyTo
2121
time
2222
}
2323

24-
attr_reader :decision_id, :configs, :getter, :api_key
24+
attr_reader :decision_id, :configs, :getter, :api_key, :client_class
2525

2626
PROPERTIES.each do |attribute|
2727
class_eval %{
@@ -31,11 +31,12 @@ def #{attribute}
3131
}
3232
end
3333

34-
def initialize(api_key, decision_id, configs)
34+
def initialize(api_key, decision_id, configs, client_class = Sift::Client)
3535
@api_key = api_key
3636
@decision_id = decision_id
3737
@configs = configs
3838
@getter = Utils::HashGetter.new(configs)
39+
@client_class = client_class
3940
end
4041

4142
def run
@@ -58,7 +59,8 @@ def run
5859
def send_request
5960
Router.post(path, {
6061
body: request_body,
61-
headers: headers
62+
headers: headers,
63+
client_class: client_class
6264
})
6365
end
6466

@@ -79,6 +81,8 @@ def errors
7981
validator.valid_order?
8082
elsif applying_to_session?
8183
validator.valid_session?
84+
elsif applying_to_content?
85+
validator.valid_content?
8286
else
8387
validator.valid_user?
8488
end

0 commit comments

Comments
 (0)