Skip to content

Add comprehensive email validation for blocked users#3

Open
everettbu wants to merge 1 commit into
blocked-email-validation-prefrom
blocked-email-validation-post
Open

Add comprehensive email validation for blocked users#3
everettbu wants to merge 1 commit into
blocked-email-validation-prefrom
blocked-email-validation-post

Conversation

@everettbu

Copy link
Copy Markdown
Contributor

Test 3

… many times each email address is blocked, and last time it was blocked. Move email validation out of User model and into EmailValidator. Signup form remembers which email addresses have failed and shows validation error on email field.
@github-actions

Copy link
Copy Markdown

This pull request has been automatically marked as stale because it has been open for 60 days with no activity. To keep it open, remove the stale tag, push code, or add a comment. Otherwise, it will be closed in 14 days.

@maloyan4good

Copy link
Copy Markdown

Code Review

Verdict: REQUEST CHANGES
Confidence: HIGH

Summary

This PR introduces a BlockedEmail model and an EmailValidator to replace the inline email_validator method in User, and adds client-side caching of rejected emails to avoid re-submitting known-bad addresses. The approach is sound, but there are several correctness and security issues that must be addressed before merging.


Findings

Priority Issue Location
P1 BlockedEmail.should_block? has a write-within-read race condition and silently swallows save errors app/models/blocked_email.rb:12-19
P1 email_in_restriction_setting? builds a regex from unsanitized user-controlled site settings — ReDoS / injection risk lib/validators/email_validator.rb:17-20
P1 values in the error response exposes all user attributes (including password digest, salt, etc.) via user.attributes app/controllers/users_controller.rb:196-197
P1 BlockedEmail.should_block? increments match_count and updates last_match_at even for do_nothing records, conflating "matched" with "blocked" app/models/blocked_email.rb:12-19
P2 and used instead of && in validator — low precedence can cause subtle bugs lib/validators/email_validator.rb:13
P2 Missing newline at end of file lib/validators/email_validator.rb:24
P2 rejectedEmails client cache is never cleared on email field change — stale rejections persist across edits app/assets/javascripts/discourse/controllers/create_account_controller.js
P3 BlockedEmail.should_block? uses .where(...).first instead of .find_by app/models/blocked_email.rb:13

Details

[P1] Race condition + silent failure in should_block?

File: app/models/blocked_email.rb:12-19

match_count is incremented in Ruby and then saved. Under concurrent requests for the same email, two threads can both read match_count = N, both write N+1, and one increment is lost. Additionally, record.save can fail (e.g. validation error) and the failure is silently ignored.

Suggested fix:

def self.should_block?(email)
  record = BlockedEmail.find_by(email: email)
  if record
    BlockedEmail.where(email: email)
                .update_all('match_count = match_count + 1, last_match_at = NOW()')
  end
  record && record.action_type == actions[:block]
end

[P1] Regex injection / ReDoS in email_in_restriction_setting?

File: lib/validators/email_validator.rb:17-20

The setting value is only partially sanitized (dots are escaped) but other regex metacharacters ((, ), *, +, ?, etc.) are passed through verbatim. A malicious or misconfigured site setting can cause catastrophic backtracking or inject arbitrary regex patterns.

Suggested fix:

def email_in_restriction_setting?(setting, value)
  domains = setting.split(/[\s,]+/).map { |d| Regexp.escape(d.strip) }.join('|')
  regexp = Regexp.new("@(#{domains})\z", true)
  value =~ regexp
end

[P1] user.attributes leaks sensitive fields

File: app/controllers/users_controller.rb:196-197

user.attributes returns every column in the users table, including password_hash, salt, auth_token, and any other sensitive columns. Only the three fields actually needed should be sliced.

Suggested fix:

values: { name: user.name, username: user.username, email: user.email }

(Replace user.attributes.slice("name", "username", "email") with explicit attribute access to avoid accidentally including future sensitive columns.)


[P1] Statistics updated for do_nothing records — semantic mismatch

File: app/models/blocked_email.rb:12-19

match_count and last_match_at are updated regardless of action_type. The field names imply "how many times was this email blocked", but they are incremented even when the action is do_nothing. This makes the audit data misleading. The stat update should be unconditional (tracking matches, not blocks) and the fields should be renamed, or the update should only happen when action_type == :block. Either way, the intent should be made explicit.


[P2] and vs && operator precedence

File: lib/validators/email_validator.rb:13

if record.errors[attribute].blank? and BlockedEmail.should_block?(value)

and has very low precedence in Ruby. While it works here because there is no assignment, it is a style hazard and inconsistent with the rest of the codebase. Use && instead.


[P2] Client-side rejectedEmails cache never invalidated

File: app/assets/javascripts/discourse/controllers/create_account_controller.js

Once an email is added to rejectedEmails, it stays there for the lifetime of the modal. If the user corrects a typo and re-enters a previously rejected email that is now valid (e.g. the admin removed the block), the UI will still show it as invalid. Consider clearing the cache when the email field changes, or scoping the check more narrowly.


Recommendation

Fix the three P1 issues before merging: use update_all for atomic stat updates, sanitize the restriction-setting regex with Regexp.escape, and replace user.attributes.slice with explicit field access in the controller. The do_nothing stat-tracking semantics should also be clarified. The P2 items are minor but easy to fix in the same pass.

@mfeuerstein mfeuerstein left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review — approved

Reviewed 10 files. 0 high-severity issues found. Verdict: approved.

app/models/blocked_email.rb (low)

  • Reviewed app/models/blocked_email.rb — looks good

db/migrate/20130724201552_create_blocked_emails.rb (low)

  • Reviewed db/migrate/20130724201552_create_blocked_emails.rb — looks good

app/controllers/users_controller.rb (low)

  • Reviewed app/controllers/users_controller.rb — looks good

spec/components/validators/email_validator_spec.rb (low)

  • Reviewed spec/components/validators/email_validator_spec.rb — looks good

config/locales/server.en.yml (low)

  • Reviewed config/locales/server.en.yml — looks good

lib/validators/email_validator.rb (low)

  • Reviewed lib/validators/email_validator.rb — looks good

app/models/user.rb (low)

  • Reviewed app/models/user.rb — looks good

app/assets/javascripts/discourse/controllers/create_account_controller.js (low)

  • Reviewed app/assets/javascripts/discourse/controllers/create_account_controller.js — looks good

spec/fabricators/blocked_email_fabricator.rb (low)

  • Reviewed spec/fabricators/blocked_email_fabricator.rb — looks good

spec/models/blocked_email_spec.rb (low)

  • Reviewed spec/models/blocked_email_spec.rb — looks good

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants