-
Notifications
You must be signed in to change notification settings - Fork 9
Add comprehensive email validation for blocked users #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: blocked-email-validation-pre
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,25 @@ | ||||||
| class BlockedEmail < ActiveRecord::Base | ||||||
|
|
||||||
| before_validation :set_defaults | ||||||
|
|
||||||
| validates :email, presence: true, uniqueness: true | ||||||
|
|
||||||
| def self.actions | ||||||
| @actions ||= Enum.new(:block, :do_nothing) | ||||||
| end | ||||||
|
|
||||||
| def self.should_block?(email) | ||||||
| record = BlockedEmail.where(email: email).first | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: Using
Suggested change
|
||||||
| if record | ||||||
| record.match_count += 1 | ||||||
| record.last_match_at = Time.zone.now | ||||||
| record.save | ||||||
|
Comment on lines
+14
to
+16
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: The increment and save operations should use |
||||||
| end | ||||||
| record && record.action_type == actions[:block] | ||||||
| end | ||||||
|
Comment on lines
+11
to
+19
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: This method has side effects during a read operation - it updates statistics even when just checking if an email should be blocked. This could cause race conditions in concurrent environments and makes the method name misleading. |
||||||
|
|
||||||
| def set_defaults | ||||||
| self.action_type ||= BlockedEmail.actions[:block] | ||||||
| end | ||||||
|
|
||||||
| end | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| class CreateBlockedEmails < ActiveRecord::Migration | ||
| def change | ||
| create_table :blocked_emails do |t| | ||
| t.string :email, null: false | ||
| t.integer :action_type, null: false | ||
| t.integer :match_count, null: false, default: 0 | ||
| t.datetime :last_match_at | ||
| t.timestamps | ||
| end | ||
| add_index :blocked_emails, :email, unique: true | ||
| end | ||
| end |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,24 @@ | ||||||
| class EmailValidator < ActiveModel::EachValidator | ||||||
|
|
||||||
| def validate_each(record, attribute, value) | ||||||
| if (setting = SiteSetting.email_domains_whitelist).present? | ||||||
| unless email_in_restriction_setting?(setting, value) | ||||||
| record.errors.add(attribute, I18n.t(:'user.email.not_allowed')) | ||||||
| end | ||||||
| elsif (setting = SiteSetting.email_domains_blacklist).present? | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: Using |
||||||
| if email_in_restriction_setting?(setting, value) | ||||||
| record.errors.add(attribute, I18n.t(:'user.email.not_allowed')) | ||||||
| end | ||||||
| end | ||||||
| if record.errors[attribute].blank? and BlockedEmail.should_block?(value) | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: Use
Suggested change
|
||||||
| record.errors.add(attribute, I18n.t(:'user.email.blocked')) | ||||||
| end | ||||||
| end | ||||||
|
|
||||||
| def email_in_restriction_setting?(setting, value) | ||||||
| domains = setting.gsub('.', '\.') | ||||||
| regexp = Regexp.new("@(#{domains})", true) | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: Regex pattern
Suggested change
|
||||||
| value =~ regexp | ||||||
| end | ||||||
|
Comment on lines
+18
to
+22
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: ReDoS vulnerability: |
||||||
|
|
||||||
| end | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| require 'spec_helper' | ||
|
|
||
| describe EmailValidator do | ||
|
|
||
| let(:record) { Fabricate.build(:user, email: "[email protected]") } | ||
| let(:validator) { described_class.new({attributes: :email}) } | ||
| subject(:validate) { validator.validate_each(record,:email,record.email) } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: Missing space after comma in method call parameters |
||
|
|
||
| context "blocked email" do | ||
| it "doesn't add an error when email doesn't match a blocked email" do | ||
| BlockedEmail.stubs(:should_block?).with(record.email).returns(false) | ||
| validate | ||
| record.errors[:email].should_not be_present | ||
| end | ||
|
|
||
| it "adds an error when email matches a blocked email" do | ||
| BlockedEmail.stubs(:should_block?).with(record.email).returns(true) | ||
| validate | ||
| record.errors[:email].should be_present | ||
| end | ||
| end | ||
|
|
||
| end | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| Fabricator(:blocked_email) do | ||
| email { sequence(:email) { |n| "bad#{n}@spammers.org" } } | ||
| action_type BlockedEmail.actions[:block] | ||
| end |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,48 @@ | ||||||
| require 'spec_helper' | ||||||
|
|
||||||
| describe BlockedEmail do | ||||||
|
|
||||||
| let(:email) { '[email protected]' } | ||||||
|
|
||||||
| describe "new record" do | ||||||
| it "sets a default action_type" do | ||||||
| BlockedEmail.create(email: email).action_type.should == BlockedEmail.actions[:block] | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: Use modern RSpec syntax:
Suggested change
|
||||||
| end | ||||||
|
|
||||||
| it "last_match_at is null" do | ||||||
| # If we manually load the table with some emails, we can see whether those emails | ||||||
| # have ever been blocked by looking at last_match_at. | ||||||
| BlockedEmail.create(email: email).last_match_at.should be_nil | ||||||
| end | ||||||
| end | ||||||
|
|
||||||
| describe "#should_block?" do | ||||||
| subject { BlockedEmail.should_block?(email) } | ||||||
|
|
||||||
| it "returns false if a record with the email doesn't exist" do | ||||||
| subject.should be_false | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: Use modern RSpec syntax:
Suggested change
|
||||||
| end | ||||||
|
|
||||||
| shared_examples "when a BlockedEmail record matches" do | ||||||
| it "updates statistics" do | ||||||
| Timecop.freeze(Time.zone.now) do | ||||||
| expect { subject }.to change { blocked_email.reload.match_count }.by(1) | ||||||
| blocked_email.last_match_at.should be_within_one_second_of(Time.zone.now) | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. logic: Custom matcher
Suggested change
|
||||||
| end | ||||||
| end | ||||||
| end | ||||||
|
|
||||||
| context "action_type is :block" do | ||||||
| let!(:blocked_email) { Fabricate(:blocked_email, email: email, action_type: BlockedEmail.actions[:block]) } | ||||||
| it { should be_true } | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. style: Use modern RSpec syntax:
Suggested change
|
||||||
| include_examples "when a BlockedEmail record matches" | ||||||
| end | ||||||
|
|
||||||
| context "action_type is :do_nothing" do | ||||||
| let!(:blocked_email) { Fabricate(:blocked_email, email: email, action_type: BlockedEmail.actions[:do_nothing]) } | ||||||
| it { should be_false } | ||||||
| include_examples "when a BlockedEmail record matches" | ||||||
| end | ||||||
| end | ||||||
|
|
||||||
| end | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
style: Using generic 'invalid' message instead of specific 'blocked' message may confuse users who don't understand why their email was rejected