Skip to content

Commit af8828f

Browse files
authored
Use secure_compare during signature verification (#553)
1 parent f2423d4 commit af8828f

File tree

5 files changed

+79
-2
lines changed

5 files changed

+79
-2
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
### 2.5.3 (Next)
22

33
* [#549](https://github.com/slack-ruby/slack-ruby-client/pull/549): Add group ID formatting support for message mentions - [@n0h0](https://github.com/n0h0).
4+
* [#553](https://github.com/slack-ruby/slack-ruby-client/pull/553): Use `secure_compare` during signature verification - [@hieuk09](https://github.com/hieuk09).
45
* Your contribution here.
56

67
### 2.5.2 (2025/02/19)

lib/slack-ruby-client.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
begin
2323
require 'openssl'
2424
rescue LoadError # rubocop:disable Lint/SuppressedException
25-
# Used in slack/web/config
25+
# Used in slack/web/config and slack/utils/security
2626
end
2727
require_relative 'slack/web/config'
2828
require_relative 'slack/web/api/errors/slack_error'
@@ -57,3 +57,5 @@
5757
# Events API
5858
require_relative 'slack/events/config'
5959
require_relative 'slack/events/request'
60+
61+
require_relative 'slack/utils/security'

lib/slack/events/request.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# frozen_string_literal: true
2+
23
module Slack
34
module Events
45
class Request
@@ -59,7 +60,7 @@ def valid?
5960
signature_basestring = [version, timestamp, body].join(':')
6061
hex_hash = OpenSSL::HMAC.hexdigest(digest, signing_secret, signature_basestring)
6162
computed_signature = [version, hex_hash].join('=')
62-
computed_signature == signature
63+
Utils::Security.secure_compare(computed_signature, signature)
6364
end
6465

6566
# Validates the request signature and its expiration.

lib/slack/utils/security.rb

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# frozen_string_literal: true
2+
3+
# This module is copied from activesupport
4+
# https://github.com/rails/rails/blob/3235827585d87661942c91bc81f64f56d710f0b2/activesupport/lib/active_support/security_utils.rb
5+
module Slack
6+
module Utils
7+
# rubocop:disable Naming/MethodParameterName
8+
module Security
9+
# Constant time string comparison, for fixed length strings.
10+
#
11+
# The values compared should be of fixed length, such as strings
12+
# that have already been processed by HMAC. Raises in case of length mismatch.
13+
14+
if defined?(OpenSSL.fixed_length_secure_compare)
15+
def fixed_length_secure_compare(a, b)
16+
OpenSSL.fixed_length_secure_compare(a, b)
17+
end
18+
else
19+
def fixed_length_secure_compare(a, b)
20+
raise ArgumentError, 'inputs must be of equal length' unless a.bytesize == b.bytesize
21+
22+
l = a.unpack "C#{a.bytesize}"
23+
24+
res = 0
25+
b.each_byte { |byte| res |= byte ^ l.shift }
26+
res.zero?
27+
end
28+
end
29+
module_function :fixed_length_secure_compare
30+
31+
# Secure string comparison for strings of variable length.
32+
#
33+
# While a timing attack would not be able to discern the content of
34+
# a secret compared via secure_compare, it is possible to determine
35+
# the secret length. This should be considered when using secure_compare
36+
# to compare weak, short secrets to user input.
37+
def secure_compare(a, b)
38+
a.bytesize == b.bytesize && fixed_length_secure_compare(a, b)
39+
end
40+
module_function :secure_compare
41+
end
42+
# rubocop:enable Naming/MethodParameterName
43+
end
44+
end

spec/slack/utils/security_spec.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# frozen_string_literal: true
2+
3+
require 'slack/utils/security'
4+
5+
RSpec.describe Slack::Utils::Security do
6+
describe '.secure_compare' do
7+
it 'performs string comparison correctly' do
8+
expect(described_class.secure_compare('a', 'a')).to be true
9+
expect(described_class.secure_compare('a', 'b')).to be false
10+
end
11+
12+
it 'returns false on bytesize mismatch' do
13+
expect(described_class.secure_compare('a', "\u{ff41}")).to be false
14+
end
15+
end
16+
17+
describe '.fixed_length_secure_compare' do
18+
it 'performs string comparison correctly' do
19+
expect(described_class.fixed_length_secure_compare('a', 'a')).to be true
20+
expect(described_class.fixed_length_secure_compare('a', 'b')).to be false
21+
end
22+
23+
it 'raises ArgumentError on length mismatch' do
24+
expect do
25+
described_class.fixed_length_secure_compare('a', 'ab')
26+
end.to raise_error(ArgumentError, 'inputs must be of equal length')
27+
end
28+
end
29+
end

0 commit comments

Comments
 (0)