Skip to content

Commit 40144e2

Browse files
committed
Tighten log sanitization coverage
1 parent 77b3358 commit 40144e2

3 files changed

Lines changed: 73 additions & 10 deletions

File tree

app/web/security/log_sanitizer.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,20 @@ def feed_suffix(path)
4747
# @param value [Object]
4848
# @return [Object]
4949
def sanitize_value(key, value)
50-
return sanitize_url(value) if key.to_sym == :url
5150
return sanitize_details(value) if value.is_a?(Hash)
5251
return value.map { |entry| sanitize_value(key, entry) } if value.is_a?(Array)
52+
return sanitize_url(value) if url_key?(key)
5353

5454
value
5555
end
5656

57+
# @param key [Object]
58+
# @return [Boolean]
59+
def url_key?(key)
60+
key_name = key.to_s
61+
key_name == 'url' || key_name.end_with?('_url', '_urls')
62+
end
63+
5764
# @param value [Object]
5865
# @return [Hash{Symbol=>Object}, Object]
5966
def sanitize_url(value)

spec/html2rss/web/boot/setup_spec.rb

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,30 @@
55
require_relative '../../../../app'
66

77
RSpec.describe Html2rss::Web::Boot::Setup do
8-
describe '.call!' do
9-
before do
10-
allow(Html2rss::Web::EnvironmentValidator).to receive(:validate_environment!)
11-
allow(Html2rss::Web::EnvironmentValidator).to receive(:validate_production_security!)
12-
allow(Html2rss::Web::Flags).to receive(:validate!)
13-
end
8+
before do
9+
allow(Html2rss::Web::EnvironmentValidator).to receive(:validate_environment!)
10+
allow(Html2rss::Web::EnvironmentValidator).to receive(:validate_production_security!)
11+
allow(Html2rss::Web::Flags).to receive(:validate!)
12+
end
1413

14+
describe '.call!' do
1515
it 'validates environment state', :aggregate_failures do
1616
described_class.call!
1717

1818
expect(Html2rss::Web::EnvironmentValidator).to have_received(:validate_environment!).once
1919
expect(Html2rss::Web::EnvironmentValidator).to have_received(:validate_production_security!).once
2020
expect(Html2rss::Web::Flags).to have_received(:validate!).once
2121
end
22+
23+
it 'routes rack-timeout logs through the shared app logger' do
24+
stub_const('Rack::Timeout::Logger', Class.new)
25+
logger_holder = { value: nil }
26+
Rack::Timeout::Logger.define_singleton_method(:logger=) { |value| logger_holder[:value] = value }
27+
Rack::Timeout::Logger.define_singleton_method(:logger) { logger_holder[:value] }
28+
29+
described_class.call!
30+
31+
expect(Rack::Timeout::Logger.logger).to be(Html2rss::Web::AppLogger.logger)
32+
end
2233
end
2334
end

spec/html2rss/web/log_sanitizer_spec.rb

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
expected_url = {
4949
host: 'news.ycombinator.com',
5050
scheme: 'https',
51-
hash: Digest::SHA256.hexdigest('https://news.ycombinator.com')[0..11]
51+
hash: url_hash('https://news.ycombinator.com')
5252
}
5353

5454
expect(described_class.sanitize_details(url: 'https://news.ycombinator.com')).to eq(url: expected_url)
@@ -60,6 +60,14 @@
6060
)
6161
end
6262

63+
it 'sanitizes nested url fields when emitting shared log events' do
64+
Html2rss::Web::LogEvent.emit(payload: nested_url_payload)
65+
66+
payload = JSON.parse(io.string.lines.last, symbolize_names: true)
67+
68+
expect(payload.slice(:url, :related_urls, :details)).to eq(expected_nested_url_payload)
69+
end
70+
6371
it 'sanitizes security logger token usage fields' do
6472
Html2rss::Web::SecurityLogger.log_token_usage('very-secret-token', 'https://news.ycombinator.com', true)
6573
payload = JSON.parse(io.string.lines.last, symbolize_names: true)
@@ -69,7 +77,7 @@
6977
url: {
7078
host: 'news.ycombinator.com',
7179
scheme: 'https',
72-
hash: Digest::SHA256.hexdigest('https://news.ycombinator.com')[0..11]
80+
hash: url_hash('https://news.ycombinator.com')
7381
},
7482
token_hash: Digest::SHA256.hexdigest('very-secret-token')[0..7]
7583
)
@@ -88,7 +96,7 @@
8896
expect(observability_payload.dig(:details, :url)).to eq(
8997
host: 'news.ycombinator.com',
9098
scheme: 'https',
91-
hash: Digest::SHA256.hexdigest('https://news.ycombinator.com')[0..11]
99+
hash: url_hash('https://news.ycombinator.com')
92100
)
93101
end
94102

@@ -103,4 +111,41 @@
103111
state: 'completed'
104112
)
105113
end
114+
115+
private
116+
117+
# @return [Hash{Symbol=>Object}]
118+
def nested_url_payload
119+
{
120+
url: 'https://news.ycombinator.com',
121+
related_urls: ['https://example.com/feed.xml'],
122+
details: { url: 'https://lobste.rs/s/test' }
123+
}
124+
end
125+
126+
# @return [Hash{Symbol=>Object}]
127+
def expected_nested_url_payload
128+
{
129+
url: sanitized_url('news.ycombinator.com', 'https://news.ycombinator.com'),
130+
related_urls: [
131+
sanitized_url('example.com', 'https://example.com/feed.xml')
132+
],
133+
details: {
134+
url: sanitized_url('lobste.rs', 'https://lobste.rs/s/test')
135+
}
136+
}
137+
end
138+
139+
# @param host [String]
140+
# @param url [String]
141+
# @return [Hash{Symbol=>String}]
142+
def sanitized_url(host, url)
143+
{ host:, scheme: 'https', hash: url_hash(url) }
144+
end
145+
146+
# @param url [String]
147+
# @return [String]
148+
def url_hash(url)
149+
Digest::SHA256.hexdigest(url)[0..11]
150+
end
106151
end

0 commit comments

Comments
 (0)