Skip to content

Commit

Permalink
(feat) Add hashing of sensitive data to the Ruby gem (#952)
Browse files Browse the repository at this point in the history
| 🚥 Resolves CX-674 |
| :------------------- |

## 🧰 Changes

Masks the `Authorization` header and API keys sent via the Ruby Rack
middleware.

## 🧬 QA & Testing

Added automated tests to verify the sensitive data gets hashed properly
and matches the same expected output as the Node.js app

---------

Co-authored-by: Kenny Hoxworth <[email protected]>
  • Loading branch information
hoxworth and Kenny Hoxworth authored Jan 24, 2024
1 parent c012cac commit 5197475
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 4 deletions.
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,11 @@ test-webhooks-python-flask: ## Run webhooks tests against the Python SDK + Flask
test-metrics-ruby-rails: ## Run Metrics tests against the Ruby SDK + Rails
docker-compose up --build --detach integration_ruby_rails
sleep 5
npm run test:integration-metrics || make cleanup-failure
SUPPORTS_HASHING=true npm run test:integration-metrics || make cleanup-failure
@make cleanup

test-webhooks-ruby-rails: ## Run webhooks tests against the Ruby SDK + Rails
docker-compose up --build --detach integration_ruby_rails
sleep 5
npm run test:integration-webhooks || make cleanup-failure
SUPPORTS_HASHING=true npm run test:integration-webhooks || make cleanup-failure
@make cleanup
6 changes: 6 additions & 0 deletions packages/ruby/lib/readme/http_request.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
require 'readme/mask'
require 'rack'
require 'rack/request'
require_relative 'content_type_helper'
Expand Down Expand Up @@ -25,7 +26,12 @@ class HttpRequest
HTTP_NON_HEADERS.freeze

def initialize(env)
# Sanitize the auth header, if it exists
if env.has_key?("HTTP_AUTHORIZATION")
env["HTTP_AUTHORIZATION"] = Readme::Mask.mask(env["HTTP_AUTHORIZATION"])
end
@request = Rack::Request.new(env)

return unless IS_RACK_V3

@input = Rack::RewindableInput.new(@request.body)
Expand Down
11 changes: 11 additions & 0 deletions packages/ruby/lib/readme/mask.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
require 'digest'

module Readme
class Mask
def self.mask(data)
digest = Digest::SHA2.new(512).base64digest(data)
opts = data.length >= 4 ? data[-4,4] : data
"sha512-#{digest}?#{opts}"
end
end
end
2 changes: 2 additions & 0 deletions packages/ruby/lib/readme/payload.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
require 'readme/mask'
require 'socket'
require 'securerandom'

Expand All @@ -15,6 +16,7 @@ def initialize(har, info, ip_address, development:)
@har = har
@user_info = info.slice(:id, :label, :email)
@user_info[:id] = info[:api_key] unless info[:api_key].nil? # swap api_key for id if api_key is present
@user_info[:id] = Readme::Mask.mask(@user_info[:id])
@log_id = info[:log_id]
@ignore = info[:ignore]
@ip_address = ip_address
Expand Down
35 changes: 35 additions & 0 deletions packages/ruby/spec/readme/http_request_spec.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'readme/http_request'
require 'readme/mask'

RSpec.describe Readme::HttpRequest do
describe '#url' do
Expand Down Expand Up @@ -159,6 +160,40 @@
}
)
end

it 'properly sanitizes authorization headers' do
env = {
'HTTP_AUTHORIZATION' => 'Basic xxx:aaa'
}

env['HTTP_VERSION'] = 'HTTP/1.1' unless Readme::HttpRequest::IS_RACK_V3

request = described_class.new(env)

expect(request.headers).to eq(
{
'Authorization' => Readme::Mask.mask('Basic xxx:aaa'),
}
)
end


it 'matches the hashing output of the node.js SDK' do
env = {
'HTTP_AUTHORIZATION' => 'Bearer: a-random-api-key'
}

env['HTTP_VERSION'] = 'HTTP/1.1' unless Readme::HttpRequest::IS_RACK_V3

request = described_class.new(env)

expect(request.headers).to eq(
{
'Authorization' => 'sha512-7S+L0vUE8Fn6HI3836rtz4b6fVf6H4JFur6SGkOnL3bFpC856+OSZkpIHphZ0ipNO+kUw1ePb5df2iYrNQCpXw==?-key'
}
)
end

end

describe '#body' do
Expand Down
10 changes: 8 additions & 2 deletions packages/ruby/spec/readme/payload_spec.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
require 'readme/payload'
require 'readme/mask'
require 'socket'
require 'json'
require 'securerandom'

har_json = File.read(File.expand_path('../../fixtures/har.json', __FILE__))
Expand All @@ -9,24 +11,28 @@
let(:ip_address) { Socket.ip_address_list.detect(&:ipv4_private?).ip_address }

it 'returns JSON matching the payload schema' do
id = '1'
result = described_class.new(
har,
{ id: '1', label: 'Owlbert', email: '[email protected]' },
{ id: id, label: 'Owlbert', email: '[email protected]' },
ip_address,
development: true
)

expect(JSON.parse(result.to_json)["group"]["id"]).to match(Readme::Mask.mask(id))
expect(result.to_json).to match_json_schema('payload')
end

it 'substitutes api_key for id' do
api_key = '1'
result = described_class.new(
har,
{ api_key: '1', label: 'Owlbert', email: '[email protected]' },
{ api_key: api_key, label: 'Owlbert', email: '[email protected]' },
ip_address,
development: true
)

expect(JSON.parse(result.to_json)["group"]["id"]).to match(Readme::Mask.mask(api_key))
expect(result.to_json).to match_json_schema('payload')
end

Expand Down

0 comments on commit 5197475

Please sign in to comment.