From 5197475ecc66384e3a3c2c59ad350fc2d964a33f Mon Sep 17 00:00:00 2001 From: Kenny Hoxworth Date: Tue, 23 Jan 2024 16:31:05 -0800 Subject: [PATCH] (feat) Add hashing of sensitive data to the Ruby gem (#952) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit | 🚥 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 --- Makefile | 4 +-- packages/ruby/lib/readme/http_request.rb | 6 ++++ packages/ruby/lib/readme/mask.rb | 11 ++++++ packages/ruby/lib/readme/payload.rb | 2 ++ .../ruby/spec/readme/http_request_spec.rb | 35 +++++++++++++++++++ packages/ruby/spec/readme/payload_spec.rb | 10 ++++-- 6 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 packages/ruby/lib/readme/mask.rb diff --git a/Makefile b/Makefile index f128968aa2..03f038635f 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/packages/ruby/lib/readme/http_request.rb b/packages/ruby/lib/readme/http_request.rb index 5fdd684034..2657077e02 100644 --- a/packages/ruby/lib/readme/http_request.rb +++ b/packages/ruby/lib/readme/http_request.rb @@ -1,3 +1,4 @@ +require 'readme/mask' require 'rack' require 'rack/request' require_relative 'content_type_helper' @@ -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) diff --git a/packages/ruby/lib/readme/mask.rb b/packages/ruby/lib/readme/mask.rb new file mode 100644 index 0000000000..f9f2d53ca2 --- /dev/null +++ b/packages/ruby/lib/readme/mask.rb @@ -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 diff --git a/packages/ruby/lib/readme/payload.rb b/packages/ruby/lib/readme/payload.rb index ed174e41b7..b3ee4d07f2 100644 --- a/packages/ruby/lib/readme/payload.rb +++ b/packages/ruby/lib/readme/payload.rb @@ -1,3 +1,4 @@ +require 'readme/mask' require 'socket' require 'securerandom' @@ -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 diff --git a/packages/ruby/spec/readme/http_request_spec.rb b/packages/ruby/spec/readme/http_request_spec.rb index e4570b0619..07170301e4 100644 --- a/packages/ruby/spec/readme/http_request_spec.rb +++ b/packages/ruby/spec/readme/http_request_spec.rb @@ -1,4 +1,5 @@ require 'readme/http_request' +require 'readme/mask' RSpec.describe Readme::HttpRequest do describe '#url' do @@ -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 diff --git a/packages/ruby/spec/readme/payload_spec.rb b/packages/ruby/spec/readme/payload_spec.rb index 8a6ccc5612..009d7725ff 100644 --- a/packages/ruby/spec/readme/payload_spec.rb +++ b/packages/ruby/spec/readme/payload_spec.rb @@ -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__)) @@ -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: 'owlbert@example.com' }, + { id: id, label: 'Owlbert', email: 'owlbert@example.com' }, 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: 'owlbert@example.com' }, + { api_key: api_key, label: 'Owlbert', email: 'owlbert@example.com' }, 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