Skip to content

Commit 3c50412

Browse files
committed
Add migration to purge page URL query strings
1 parent e5d3e83 commit 3c50412

File tree

3 files changed

+74
-8
lines changed

3 files changed

+74
-8
lines changed

app/models/better_together/metrics/page_view.rb

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
module BetterTogether
55
module Metrics
66
class PageView < ApplicationRecord # rubocop:todo Style/Documentation
7+
SENSITIVE_QUERY_PARAMS = %w[token password secret].freeze
8+
79
belongs_to :pageable, polymorphic: true
810

911
# Validations
@@ -14,16 +16,38 @@ class PageView < ApplicationRecord # rubocop:todo Style/Documentation
1416

1517
# Add a method to set the page_url automatically if the pageable responds to a `url` method
1618
before_validation :set_page_url
19+
validate :page_url_without_sensitive_parameters
1720

1821
private
1922

23+
attr_reader :page_url_query
24+
2025
# Set the page_url if the pageable object doesn't respond to :url
21-
def set_page_url
22-
if pageable.respond_to?(:url)
23-
self.page_url = pageable.becomes(pageable.class.base_class).url
24-
elsif pageable.present? && page_url.blank?
25-
self.page_url = generate_url_for_pageable
26-
end
26+
def set_page_url # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
27+
url = if pageable.respond_to?(:url)
28+
pageable.becomes(pageable.class.base_class).url
29+
elsif pageable.present? && page_url.blank?
30+
generate_url_for_pageable
31+
else
32+
page_url
33+
end
34+
35+
return if url.blank?
36+
37+
uri = URI.parse(url)
38+
@page_url_query = uri.query
39+
self.page_url = uri.path
40+
rescue URI::InvalidURIError
41+
errors.add(:page_url, 'is invalid')
42+
end
43+
44+
def page_url_without_sensitive_parameters
45+
return if page_url_query.blank?
46+
47+
params = Rack::Utils.parse_nested_query(page_url_query)
48+
return unless params.keys.intersect?(SENSITIVE_QUERY_PARAMS)
49+
50+
errors.add(:page_url, 'contains sensitive parameters')
2751
end
2852

2953
# Generate the URL for the pageable using `url_for`
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# frozen_string_literal: true
2+
3+
# Removes query strings from existing page_url entries
4+
class PurgeQueryStringsFromPageUrls < ActiveRecord::Migration[7.1]
5+
class PageView < ApplicationRecord # rubocop:todo Style/Documentation
6+
self.table_name = 'better_together_metrics_page_views'
7+
end
8+
9+
def up
10+
PageView.where("page_url LIKE '%?%'").find_each do |pv|
11+
uri = URI.parse(pv.page_url)
12+
PageView.where(id: pv.id).update_all(page_url: uri.path)
13+
rescue URI::InvalidURIError
14+
PageView.where(id: pv.id).update_all(page_url: nil)
15+
end
16+
end
17+
18+
def down
19+
# irreversible
20+
end
21+
end

spec/models/better_together/metrics/page_view_spec.rb

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,29 @@
44

55
module BetterTogether
66
RSpec.describe Metrics::PageView, type: :model do
7-
it 'exists' do
8-
expect(described_class).to be
7+
let(:viewed_at) { Time.zone.now }
8+
let(:locale) { 'en' }
9+
10+
it 'normalizes page_url to exclude query strings' do
11+
page_view = described_class.new(
12+
page_url: 'https://example.com/path?foo=bar',
13+
viewed_at: viewed_at,
14+
locale: locale
15+
)
16+
17+
expect(page_view).to be_valid
18+
expect(page_view.page_url).to eq('/path')
19+
end
20+
21+
it 'rejects URLs containing sensitive parameters' do
22+
page_view = described_class.new(
23+
page_url: 'https://example.com/path?token=abc',
24+
viewed_at: viewed_at,
25+
locale: locale
26+
)
27+
28+
expect(page_view).not_to be_valid
29+
expect(page_view.errors[:page_url]).to include('contains sensitive parameters')
930
end
1031
end
1132
end

0 commit comments

Comments
 (0)