discourse/spec/services/problem_check_spec.rb
Penar Musaraj f104fe160a
FIX: Allow only sanitized strings in admin notices (#39796)
Prevents bad translations or bad metadata from leaking into the admin
dashboard via the outputted admin notices.
2026-05-06 13:40:20 -04:00

240 lines
7.4 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe ProblemCheck do
around do |example|
ScheduledCheck = Class.new(described_class) { self.perform_every = 30.minutes }
RealtimeCheck = Class.new(described_class)
InlineCheck = Class.new(described_class) { self.inline = true }
PluginCheck = Class.new(described_class)
DisabledCheck = Class.new(described_class) { self.enabled = false }
MultiTargetCheck = Class.new(described_class) { self.targets = -> { %w[foo bar] } }
FailingCheck =
Class.new(described_class) do
def call
problem
end
def translation_key
"failing_check"
end
end
UnsafeDetailsCheck =
Class.new(described_class) do
def call
ProblemCheck::Problem.new(
"Unsafe problem",
details: {
detail: "<img src=x onerror=alert(1)>Unsafe",
link_url: 'https://example.com" onclick="alert(1)',
},
)
end
end
PassingCheck =
Class.new(described_class) do
def call
no_problem
end
def translation_key
"passing_check"
end
end
stub_const(
described_class,
"CORE_PROBLEM_CHECKS",
[
ScheduledCheck,
RealtimeCheck,
InlineCheck,
DisabledCheck,
MultiTargetCheck,
FailingCheck,
UnsafeDetailsCheck,
PassingCheck,
],
&example
)
Object.send(:remove_const, ScheduledCheck.name)
Object.send(:remove_const, RealtimeCheck.name)
Object.send(:remove_const, InlineCheck.name)
Object.send(:remove_const, DisabledCheck.name)
Object.send(:remove_const, MultiTargetCheck.name)
Object.send(:remove_const, PluginCheck.name)
Object.send(:remove_const, FailingCheck.name)
Object.send(:remove_const, UnsafeDetailsCheck.name)
Object.send(:remove_const, PassingCheck.name)
end
let(:scheduled_check) { ScheduledCheck }
let(:realtime_check) { RealtimeCheck }
let(:inline_check) { InlineCheck }
let(:enabled_check) { RealtimeCheck }
let(:disabled_check) { DisabledCheck }
let(:multi_target_check) { MultiTargetCheck }
let(:plugin_check) { PluginCheck }
let(:failing_check) { FailingCheck }
let(:unsafe_details_check) { UnsafeDetailsCheck }
let(:passing_check) { PassingCheck }
describe ".[]" do
it { expect(described_class[:scheduled_check]).to eq(scheduled_check) }
it { expect(described_class[:foo]).to eq(nil) }
end
describe ".identifier" do
it { expect(scheduled_check.identifier).to eq(:scheduled_check) }
end
describe ".checks" do
it { expect(described_class.checks).to include(scheduled_check, realtime_check, inline_check) }
end
describe ".scheduled" do
it { expect(described_class.scheduled).to include(scheduled_check) }
it { expect(described_class.scheduled).not_to include(realtime_check) }
it { expect(described_class.scheduled).not_to include(inline_check) }
end
describe ".realtime" do
it { expect(described_class.realtime).to include(realtime_check) }
it { expect(described_class.realtime).not_to include(scheduled_check) }
it { expect(described_class.realtime).not_to include(inline_check) }
end
describe ".scheduled?" do
it { expect(scheduled_check).to be_scheduled }
it { expect(realtime_check).to_not be_scheduled }
it { expect(inline_check).to_not be_scheduled }
end
describe ".realtime?" do
it { expect(realtime_check).to be_realtime }
it { expect(scheduled_check).to_not be_realtime }
it { expect(inline_check).to_not be_realtime }
end
describe ".inline?" do
it { expect(inline_check).to be_inline }
it { expect(realtime_check).to_not be_inline }
it { expect(scheduled_check).to_not be_inline }
end
describe ".enabled?" do
it { expect(enabled_check).to be_enabled }
it { expect(disabled_check).not_to be_enabled }
end
describe ".targeted?" do
it { expect(scheduled_check).not_to be_targeted }
it { expect(multi_target_check).to be_targeted }
end
describe "plugin problem check registration" do
before { DiscoursePluginRegistry.register_problem_check(PluginCheck, stub(enabled?: enabled)) }
after { DiscoursePluginRegistry.reset! }
context "when the plugin is enabled" do
let(:enabled) { true }
it { expect(described_class.checks).to include(plugin_check) }
end
context "when the plugin is disabled" do
let(:enabled) { false }
it { expect(described_class.checks).not_to include(plugin_check) }
end
end
describe "#run" do
context "when check is failing" do
it { expect { failing_check.new.run }.to change { ProblemCheckTracker.failing.count }.by(1) }
end
context "when problem details contain unsafe HTML" do
before do
I18n.backend.store_translations(
:en,
dashboard: {
problem: {
unsafe_details_check: 'Problem <a href="%{link_url}">details</a>: %{detail}',
},
},
)
end
it "sanitizes problem details rendered in admin notice messages" do
unsafe_details_check.new.run
message = AdminNotice.find_by!(identifier: "unsafe_details_check").message
aggregate_failures do
expect(message).to include('<a href="https://example.com">details</a>')
expect(message).to include("Unsafe")
expect(message).not_to include("onclick")
expect(message).not_to include("<img")
end
end
end
context "when check is passing" do
it { expect { passing_check.new.run }.to change { ProblemCheckTracker.passing.count }.by(1) }
end
context "when targeted check is initialized with no target" do
context "when a tracker exists" do
before do
ProblemCheckTracker.create!(
identifier: "multi_target_check",
target: ProblemCheck::NO_TARGET,
)
end
it "deletes the tracker" do
expect { multi_target_check.new.run }.to change { ProblemCheckTracker.count }.by(-1)
end
end
context "when a tracker does not exist" do
it "does nothing" do
expect { multi_target_check.new.run }.not_to change { ProblemCheckTracker.count }
end
end
end
context "when targeted check has an outdated target" do
before { ProblemCheckTracker.create!(identifier: "multi_target_check", target: "baz") }
it "deletes the tracker" do
expect { multi_target_check.new("baz").run }.to change { ProblemCheckTracker.count }.by(-1)
end
end
end
describe "#cleanup_trackers" do
before do
ProblemCheckTracker.create!(identifier: "multi_target_check", target: "foo")
ProblemCheckTracker.create!(identifier: "multi_target_check", target: "bar")
ProblemCheckTracker.create!(identifier: "multi_target_check", target: "baz")
end
it "deletes trackers with non-existing targets" do
expect { multi_target_check.cleanup_trackers }.to change {
ProblemCheckTracker.pluck(:target)
}.from(contain_exactly("foo", "bar", "baz")).to(contain_exactly("foo", "bar"))
end
context "when targets returns an empty array" do
before { MultiTargetCheck.targets = -> { [] } }
after { MultiTargetCheck.targets = -> { %w[foo bar] } }
it "does not delete any trackers" do
expect { multi_target_check.cleanup_trackers }.not_to change { ProblemCheckTracker.count }
end
end
end
end