CVE-2026-48780
Description
A maliciously crafted email address using RFC2047 encoded-words can bypass domain allowlist/denylist restrictions in Forem, granting unauthorized access to invite-only deployments.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A maliciously crafted email address using RFC2047 encoded-words can bypass domain allowlist/denylist restrictions in Forem, granting unauthorized access to invite-only deployments.
Vulnerability
Forem, an open-source community-building platform, is vulnerable to a domain restriction bypass prior to commit a2ab6d4. A maliciously crafted email address containing RFC2047 encoded-words (e.g., =?utf-8?q?test=40attacker.com=3e?=@company.com) can be accepted as valid by the email validation logic, allowing an attacker to bypass allowlist or denylist domain checks. The vulnerability is present in all versions before the patched commit [1].
Exploitation
An attacker needs no authentication or special privileges, only the ability to submit a registration or invitation form with a crafted email address. The attacker constructs an email that includes an RFC2047 encoded-word in the local part or domain part, such that the application's email validator considers the address syntactically valid while the underlying email delivery system may interpret the decoded address differently. This allows the attacker to bypass domain-based restrictions that would otherwise block or allow certain email domains [1][2].
Impact
Successful exploitation enables an attacker to gain unauthorized access to invite-only Forem deployments or communities that rely on domain allowlists or denylists. The attacker can register an account using an email address that appears to originate from an allowed domain, potentially leading to unauthorized disclosure of community content, ability to interact with other members, and further compromise of community integrity [2].
Mitigation
The issue is patched in commit a2ab6d4. Users should update Forem to a version that includes this commit. As a workaround, some SMTP servers and email delivery providers may drop or refuse to send maliciously crafted email addresses, but this is not a reliable mitigation. No CVE has been assigned (this is the CVE being described).
AI Insight generated on Jun 16, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
15 files changed · +108 −0
app/models/user.rb+7 −0 modified@@ -150,6 +150,7 @@ def cached_recent_user_ids validates :comments_count, presence: true validates :credits_count, presence: true validates :email, length: { maximum: 254 }, email: true, allow_nil: true + validate :reject_encoded_word_email, if: -> { email.present? } validates :email, uniqueness: { allow_nil: true, case_sensitive: false }, if: :email_changed? validates :following_orgs_count, presence: true validates :following_tags_count, presence: true @@ -866,6 +867,12 @@ def downcase_email self.email = email.downcase if email end + def reject_encoded_word_email + encoded_word_regex = /=\?[^?]+\?[BbQq]\?[^?]+\?=/ + + errors.add(:email, :invalid) if email.match?(encoded_word_regex) + end + def bust_cache Users::BustCacheWorker.perform_async(id) end
spec/models/user_spec.rb+48 −0 modified@@ -192,6 +192,54 @@ def provider_username(service_name) it { is_expected.to validate_length_of(:password).is_at_most(100).is_at_least(8) } it { is_expected.to validate_length_of(:username).is_at_most(30).is_at_least(2) } + it "rejects RFC2047 encoded-words in email with a generic invalid error" do + invalid_emails = [ + "=?utf-8?q?test=40attacker.com=3e?=@company.com", + "=?utf-8?q?test=40attacker.com=3e=00?=test@company.com", + "=?utf-8?q?test=40attacker.com=3e=0A=0D?=test@company.com", + "=?utf-8?q?test=40attacker.com=3e=3c?=test@company.com", + "=?utf-8?q?test=40attacker.com=3e=0A=0D?=RCPTTO:test@company.com", + "user=?utf-8?q?x?=@company.com", + "user@=?utf-8?q?company.com?=", + "=?utf-8?Q?test=40attacker.com=3e?=@company.com", + "=?utf-8?B?dGVzdEBhdHRhY2tlci5jb20+?=@company.com", + ] + + invalid_emails.each do |email| + user = build(:user, email: email) + + expect(user).not_to be_valid + expect(user.errors[:email]).to include("is invalid") + end + end + + it "documents that mail decoding changes the recipient shape locally" do + encoded_word_email = "=?utf-8?q?test=40attacker.com=3e?=@company.com" + + expect(Mail::Encodings.value_decode(encoded_word_email)).to eq("test@attacker.com>@company.com") + end + + it "preserves nil email behavior" do + user = build(:user, email: nil) + + expect(user).to be_valid + end + + it "keeps normal and non-encoded-word email addresses valid" do + valid_emails = [ + "user@example.com", + "first.last+tag@example.co.uk", + "user=?not-complete@example.com", + "user?utf-8?q?x@example.com", + ] + + valid_emails.each do |email| + user = build(:user, email: email) + + expect(user).to be_valid + end + end + it { is_expected.not_to allow_value(" ").for(:name) } it { is_expected.to validate_presence_of(:articles_count) }
spec/requests/magic_links_spec.rb+24 −0 modified@@ -123,6 +123,30 @@ expect(response).to redirect_to(new_user_session_path) end end + + it "does not create a user or send a magic link when encoded-word email domain is acceptable" do + encoded_word_email = "=?utf-8?q?test=40attacker.com=3e?=@company.com" + built_user = nil + + allow(ForemInstance).to receive(:invitation_only?).and_return(false) + allow(Settings::Authentication).to receive(:acceptable_domain?).with(domain: "company.com").and_return(true) + allow(User).to receive(:find_by).and_call_original + allow(User).to receive(:find_by).with(email: encoded_word_email).and_return(nil) + allow(User).to receive(:new).and_wrap_original do |method, *args, **kwargs| + built_user = method.call(*args, **kwargs) + allow(built_user).to receive(:send_magic_link!).and_call_original + built_user + end + + expect do + post "/magic_links", params: { email: encoded_word_email } + end.not_to change(User, :count) + + expect(built_user).not_to be_persisted + expect(built_user).not_to have_received(:send_magic_link!) + expect(controller.current_user).to be_nil + expect(response).to redirect_to(new_user_session_path) + end end context "when no email is provided" do
spec/requests/registrations_spec.rb+12 −0 modified@@ -430,6 +430,18 @@ def mock_recaptcha_verification expect(User.all.size).to be 0 end + it "does not create user when encoded-word email passes the raw domain allow list" do + allow(Settings::Authentication).to receive(:allowed_registration_email_domains).and_return(["company.com"]) + + post "/users", params: + { user: { name: "attacker #{rand(10)}", + username: "attacker_#{rand(10)}", + email: "=?utf-8?q?test=40attacker.com=3e?=@company.com", + password: "PaSSw0rd_yo000", + password_confirmation: "PaSSw0rd_yo000" } } + expect(User.all.size).to be 0 + end + it "creates user when email in allow list" do post "/users", params: { user: { name: "royal #{rand(10)}",
spec/services/authentication/authenticator_spec.rb+17 −0 modified@@ -340,6 +340,23 @@ ) end + it "rejects encoded-word provider email through user validation" do + encoded_word_email = "=?utf-8?q?test=40attacker.com=3e?=@company.com" + auth_payload.info.email = encoded_word_email + allow(ForemStatsClient).to receive(:increment) + allow(Settings::Authentication).to receive(:acceptable_domain?).with(domain: "company.com").and_return(true) + + expect do + expect { described_class.call(auth_payload) }.to raise_error(ActiveRecord::RecordInvalid, /Email is invalid/) + end.to not_change(User, :count).and not_change(Identity, :count) + + expect(Settings::Authentication).to have_received(:acceptable_domain?).with(domain: "company.com") + expect(User.find_by(email: encoded_word_email)).to be_nil + expect(Identity.find_by(provider: "github", uid: auth_payload.uid)).to be_nil + tags = hash_including(tags: array_including("error:ActiveRecord::RecordInvalid")) + expect(ForemStatsClient).to have_received(:increment).with("identity.errors", tags) + end + it "increments identity.errors if any errors occur in the transaction" do # rubocop:disable RSpec/AnyInstance allow_any_instance_of(Identity).to receive(:save!).and_raise(StandardError)
Vulnerability mechanics
Root cause
"Missing validation of RFC 2047 encoded-words in email addresses allows domain allowlist/denylist bypass."
Attack vector
An attacker submits a registration, magic-link, or OAuth sign-up request with an email address containing an RFC 2047 encoded-word in the local part, such as `=?utf-8?q?test=40attacker.com=3e?=@company.com`. The domain allowlist/denylist checks the raw domain (`company.com`) and approve it, but when the email is later decoded by the mail library the effective recipient becomes `test@attacker.com>@company.com`, routing messages to an attacker-controlled domain. This allows bypassing domain restrictions to gain access to invite-only deployments or to receive authentication emails at an arbitrary address [ref_id=1].
Affected code
The vulnerability exists in the `User` model (`app/models/user.rb`). The domain allowlist/denylist checks operate on the raw email string, but downstream mail libraries decode RFC 2047 encoded-words, changing the effective recipient. The patch adds a `reject_encoded_word_email` validation that detects encoded-word patterns with the regex `/=\?[^?]+\?[BbQq]\?[^?]+\?=/` [patch_id=6189714].
What the fix does
The patch adds a custom validation method `reject_encoded_word_email` to the `User` model that checks the email against the regex `/=\?[^?]+\?[BbQq]\?[^?]+\?=/` and adds an `:invalid` error if matched [patch_id=6189714]. This prevents any email containing an RFC 2047 encoded-word from being accepted, regardless of whether the raw domain passes allowlist checks. The test suite confirms that encoded-word emails are rejected during registration, magic-link requests, and OAuth authentication flows [ref_id=1].
Preconditions
- configThe Forem deployment must use a domain allowlist or denylist (e.g., Settings::Authentication.allowed_registration_email_domains) or be configured as invitation-only.
- inputThe attacker must be able to submit a registration, magic-link, or OAuth sign-up form with a crafted email address.
- authNo authentication is required; the attack is unauthenticated (CVSS PR:N).
Reproduction
No public PoC is included in the bundle.
Generated on Jun 16, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
2News mentions
0No linked articles in our index yet.