0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-11 02:59:07 +08:00
discourse/plugins/poll/spec/lib/polls_validator_spec.rb
Sam 5d21dae7d9
SECURITY: Uncontrolled Resource Consumption in Number Poll Generation (#41141)
## Summary

The markdown engine generates poll options in an unbounded loop based on
user input, enabling an attacker to trigger massive memory allocation
and CPU usage in the V8 process. This blocks a global mutex and can
cause worker crashes, effectively DoS-ing markdown processing for all
users.

## Source

- Patch Triage: https://patch.discourse.org/patch-triage/1120
- HackerOne report: https://hackerone.com/reports/3598542

---

🤖 Auto-generated from the patch diff via Patch Triage. Review carefully
before merging.

Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>

---------

Co-authored-by: discourse-patch-triage[bot] <272280883+discourse-patch-triage[bot]@users.noreply.github.com>
2026-06-24 15:20:12 +10:00

394 lines
8.8 KiB
Ruby
Vendored

# frozen_string_literal: true
RSpec.describe DiscoursePoll::PollsValidator do
subject(:validator) { described_class.new(post) }
let(:post) { Fabricate(:post) }
describe "#validate_polls" do
it "ensures that polls have valid arguments" do
raw = <<~RAW
[poll type=not_good1 status=not_good2 results=not_good3]
* 1
* 2
[/poll]
RAW
post.raw = raw
expect(post.valid?).to eq(false)
expect(post.errors[:base]).to include(
I18n.t("poll.invalid_argument", argument: "type", value: "not_good1"),
)
expect(post.errors[:base]).to include(
I18n.t("poll.invalid_argument", argument: "status", value: "not_good2"),
)
expect(post.errors[:base]).to include(
I18n.t("poll.invalid_argument", argument: "results", value: "not_good3"),
)
end
it "ensures that all possible values are valid" do
raw = <<~RAW
[poll type=regular result=always]
* 1
* 2
[/poll]
RAW
post.raw = raw
expect(post.valid?).to eq(true)
raw = <<~RAW
[poll type=multiple result=on_vote min=1 max=2]
* 1
* 2
* 3
[/poll]
RAW
post.raw = raw
expect(post.valid?).to eq(true)
raw = <<~RAW
[poll type=number result=on_close min=3 max=7]
[/poll]
RAW
post.raw = raw
expect(post.valid?).to eq(true)
end
it "ensure that polls have unique names" do
raw = <<~RAW
[poll]
* 1
* 2
[/poll]
[poll]
* 1
* 2
[/poll]
RAW
post.raw = raw
expect(post.valid?).to eq(false)
expect(post.errors[:base]).to include(I18n.t("poll.multiple_polls_without_name"))
raw = <<~RAW
[poll name=test]
* 1
* 2
[/poll]
[poll name=test]
* 1
* 2
[/poll]
RAW
post.raw = raw
expect(post.valid?).to eq(false)
expect(post.errors[:base]).to include(
I18n.t("poll.multiple_polls_with_same_name", name: "test"),
)
end
it "ensure that polls have unique options" do
raw = <<~RAW
[poll]
* 1
* 1
[/poll]
RAW
post.raw = raw
expect(post.valid?).to eq(false)
expect(post.errors[:base]).to include(I18n.t("poll.default_poll_must_have_different_options"))
raw = <<~RAW
[poll name=test]
* 1
* 1
[/poll]
RAW
post.raw = raw
expect(post.valid?).to eq(false)
expect(post.errors[:base]).to include(
I18n.t("poll.named_poll_must_have_different_options", name: "test"),
)
end
it "ensures that polls do not have any blank options" do
raw = <<~RAW
[poll]
* 1
*
[/poll]
RAW
post.raw = raw
expect(post.valid?).to eq(false)
expect(post.errors[:base]).to include(
I18n.t("poll.default_poll_must_not_have_any_empty_options"),
)
raw = <<~RAW
[poll name=test]
*
* 1
[/poll]
RAW
post.raw = raw
expect(post.valid?).to eq(false)
expect(post.errors[:base]).to include(
I18n.t("poll.named_poll_must_not_have_any_empty_options", name: "test"),
)
end
it "ensure that polls have at least 1 option" do
raw = <<~RAW
[poll]
[/poll]
RAW
post.raw = raw
expect(post.valid?).to eq(false)
expect(post.errors[:base]).to include(I18n.t("poll.default_poll_must_have_at_least_1_option"))
raw = <<~RAW
[poll name=test]
[/poll]
RAW
post.raw = raw
expect(post.valid?).to eq(false)
expect(post.errors[:base]).to include(
I18n.t("poll.named_poll_must_have_at_least_1_option", name: "test"),
)
end
it "ensure that polls options do not exceed site settings" do
SiteSetting.poll_maximum_options = 2
raw = <<~RAW
[poll]
* 1
* 2
* 3
[/poll]
RAW
post.raw = raw
expect(post.valid?).to eq(false)
expect(post.errors[:base]).to include(
I18n.t("poll.default_poll_must_have_less_options", count: SiteSetting.poll_maximum_options),
)
raw = <<~RAW
[poll name=test]
* 1
* 2
* 3
[/poll]
RAW
post.raw = raw
expect(post.valid?).to eq(false)
expect(post.errors[:base]).to include(
I18n.t(
"poll.named_poll_must_have_less_options",
name: "test",
count: SiteSetting.poll_maximum_options,
),
)
end
it "rejects number polls whose generated range exceeds poll_maximum_options" do
SiteSetting.poll_maximum_options = 20
post.raw = <<~RAW
[poll type=number min=1 max=100 step=1]
[/poll]
RAW
expect(post.valid?).to eq(false)
expect(post.errors[:base]).to include(
I18n.t("poll.default_poll_must_have_less_options", count: SiteSetting.poll_maximum_options),
)
end
describe "multiple type polls" do
it "ensure that min < max" do
raw = <<~RAW
[poll type=multiple min=2 max=1]
* 1
* 2
* 3
[/poll]
RAW
post.raw = raw
expect(post.valid?).to eq(false)
expect(post.errors[:base]).to include(
I18n.t("poll.default_poll_with_multiple_choices_has_invalid_parameters"),
)
raw = <<~RAW
[poll type=multiple min=2 max=1 name=test]
* 1
* 2
* 3
[/poll]
RAW
post.raw = raw
expect(post.valid?).to eq(false)
expect(post.errors[:base]).to include(
I18n.t("poll.named_poll_with_multiple_choices_has_invalid_parameters", name: "test"),
)
end
it "ensure max > 0" do
raw = <<~RAW
[poll type=multiple max=-2]
* 1
* 2
[/poll]
RAW
post.raw = raw
expect(post.valid?).to eq(false)
expect(post.errors[:base]).to include(
I18n.t("poll.default_poll_with_multiple_choices_has_invalid_parameters"),
)
end
it "ensure that max <= number of options" do
raw = <<~RAW
[poll type=multiple max=3]
* 1
* 2
[/poll]
RAW
post.raw = raw
expect(post.valid?).to eq(false)
expect(post.errors[:base]).to include(
I18n.t("poll.default_poll_with_multiple_choices_has_invalid_parameters"),
)
end
it "ensure that min >= 0" do
raw = <<~RAW
[poll type=multiple min=-1]
* 1
* 2
[/poll]
RAW
post.raw = raw
expect(post.valid?).to eq(false)
expect(post.errors[:base]).to include(
I18n.t("poll.default_poll_with_multiple_choices_has_invalid_parameters"),
)
end
it "ensure that min cannot be 0" do
raw = <<~RAW
[poll type=multiple min=0]
* 1
* 2
[/poll]
RAW
post.raw = raw
expect(post.valid?).to eq(false)
end
it "ensure that min != number of options" do
raw = <<~RAW
[poll type=multiple min=2]
* 1
* 2
[/poll]
RAW
post.raw = raw
expect(post.valid?).to eq(false)
expect(post.errors[:base]).to include(
I18n.t("poll.default_poll_with_multiple_choices_has_invalid_parameters"),
)
end
it "ensure that min < number of options" do
raw = <<~RAW
[poll type=multiple min=3]
* 1
* 2
[/poll]
RAW
post.raw = raw
expect(post.valid?).to eq(false)
expect(post.errors[:base]).to include(
I18n.t("poll.default_poll_with_multiple_choices_has_invalid_parameters"),
)
end
end
it "number type polls are validated" do
raw = <<~RAW
[poll type=number min=-5 max=-10 step=-1]
[/poll]
RAW
post.raw = raw
expect(post.valid?).to eq(false)
expect(post.errors[:base]).to include(
"Min #{I18n.t("errors.messages.greater_than", count: 0)}",
)
expect(post.errors[:base]).to include(
"Max #{I18n.t("errors.messages.greater_than", count: "min")}",
)
expect(post.errors[:base]).to include(
"Step #{I18n.t("errors.messages.greater_than", count: 0)}",
)
raw = <<~RAW
[poll type=number min=9999999999 max=9999999999 step=1]
[/poll]
RAW
post.raw = raw
expect(post.valid?).to eq(false)
expect(post.errors[:base]).to include(
"Min #{I18n.t("errors.messages.less_than", count: 2_147_483_647)}",
)
expect(post.errors[:base]).to include(
"Max #{I18n.t("errors.messages.less_than", count: 2_147_483_647)}",
)
expect(post.errors[:base]).to include(I18n.t("poll.default_poll_must_have_at_least_1_option"))
end
end
end