Skip to content

Commit 1a0aeb2

Browse files
committed
fix(rpc_auth): add millisecond precision to prevent false replay attack detection
1 parent ab6e247 commit 1a0aeb2

3 files changed

Lines changed: 41 additions & 2 deletions

File tree

packages/forest_admin_datasource_rpc/lib/forest_admin_datasource_rpc/Utils/rpc_client.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def call_rpc(endpoint, method: :get, payload: nil, symbolize_keys: false)
4141
faraday.ssl.verify = !ForestAdminRpcAgent::Facades::Container.cache(:debug)
4242
end
4343

44-
timestamp = Time.now.utc.iso8601
44+
timestamp = Time.now.utc.iso8601(3)
4545
signature = generate_signature(timestamp)
4646

4747
headers = {

packages/forest_admin_datasource_rpc/lib/forest_admin_datasource_rpc/Utils/sse_client.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def initialize(uri, auth_secret, &on_rpc_stop)
1717
def start
1818
return if @closed
1919

20-
timestamp = Time.now.utc.iso8601
20+
timestamp = Time.now.utc.iso8601(3)
2121
signature = generate_signature(timestamp)
2222

2323
headers = {

packages/forest_admin_rpc_agent/spec/lib/forest_admin_rpc_agent/middleware/authentication_spec.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,45 @@ module Middleware
192192
end
193193
end
194194

195+
context 'when multiple requests with millisecond timestamps (rapid fire)' do
196+
it 'allows multiple requests in the same second with different milliseconds' do
197+
# Simulate 3 rapid requests within the same second but with different milliseconds
198+
3.times do |i|
199+
timestamp_ms = Time.now.utc.iso8601(3)
200+
signature_ms = OpenSSL::HMAC.hexdigest('SHA256', secret, timestamp_ms)
201+
202+
test_env = {
203+
'HTTP_X_SIGNATURE' => signature_ms,
204+
'HTTP_X_TIMESTAMP' => timestamp_ms
205+
}
206+
207+
status, = middleware.call(test_env)
208+
expect(status).to eq(200), "Request #{i + 1} should succeed with millisecond timestamp"
209+
210+
# Simulate a tiny delay to ensure different milliseconds
211+
sleep(0.002)
212+
end
213+
end
214+
215+
it 'blocks replay with same millisecond timestamp' do
216+
# First request with millisecond precision
217+
timestamp_ms = Time.now.utc.iso8601(3)
218+
signature_ms = OpenSSL::HMAC.hexdigest('SHA256', secret, timestamp_ms)
219+
220+
env['HTTP_X_SIGNATURE'] = signature_ms
221+
env['HTTP_X_TIMESTAMP'] = timestamp_ms
222+
223+
# First request - should pass
224+
status, = middleware.call(env)
225+
expect(status).to eq(200)
226+
227+
# Second request with exact same timestamp and signature - should be blocked
228+
status, _headers, body = middleware.call(env)
229+
expect(status).to eq(401)
230+
expect(JSON.parse(body.first)).to eq({ 'error' => 'Unauthorized' })
231+
end
232+
end
233+
195234
context 'with edge cases' do
196235
it 'handles malformed ISO8601 timestamp' do
197236
env['HTTP_X_SIGNATURE'] = signature

0 commit comments

Comments
 (0)