CVE-2020-14214 (Ruby, published 2020-06-16T23:15Z)
Description: value: Zammad before 3.3.1, when Domain Based Assignment is enabled, relies on a claimed e-mail address for authorization decisions. An attacker can register a new account that will have access to all tickets of an arbitrary Organization.
Severity: MEDIUM (CVSS v3 base score: 6.5)
Weakness: CWE-863 \u2014 Incorrect Authorization
The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check. This allows attackers to bypass intended access restrictions.
Repository: https://github.com/zammad/zammad
Files changed:
app/assets/javascripts/app/controllers/email_verify.coffee
app/assets/javascripts/app/controllers/signup.coffee
app/assets/javascripts/app/controllers/user_profile.coffee
app/assets/javascripts/app/views/signup/verify.jst.eco
app/controllers/users_controller.rb
app/models/user.rb
lib/auth/internal.rb
test/browser/signup_password_change_and_reset_test.rb
Commit message: Fixes issue #2907 - Password strength settings are ignored when creating new customer accounts. Make login available to verified users only.


Vulnerable code:
    @authenticateCheckRedirect()
      success:     @success
      error:       @error
    )

  success: =>
    new Success(el: @el)

  error: =>
    new Fail(el: @el)

class Success extends App.ControllerContent
  constructor: ->
    super
    @render()

    # rerender view, e. g. on language change
    @bind 'ui:rerender', =>
      @render()

  render: =>
    @renderScreenSuccess(
      detail: 'Woo hoo! Your email address has been verified!'
    )
    delay = =>
      @navigate '#'
    @delay(delay, 20500)

class Fail extends App.ControllerContent
  constructor: ->
    super
    @render()

    # rerender view, e. g. on language change
    @bind 'ui:rerender', =>
      @render()

  render: =>
    @renderScreenError(
      detail: 'Unable to verify email. Please contact your administrator.'
        App.Auth.login(
          data:
            username: @params.login
            password: @params.password
          success: @success
          error: @error
        @form.showAlert(details.error_human || details.error || 'Unable to update object!')
  success: (data, status, xhr) =>
    # login check
    App.Auth.loginCheck()
    # add notify
    @notify
      type:      'success'
      msg:       App.i18n.translateContent('Thanks for joining. Email sent to "%s". Please verify your email address.', @params.email)
      removeAll: true

    # redirect to #
    @navigate '#'
class Widget extends App.Controller
  constructor: ->

    # for browser test
    App.Event.bind('user_signup_verify', (user) ->
      new Modal(user: user)
      'user_signup_verify'
    )

    App.Event.bind('auth:login', (user) =>
      return if !user
      @verifyLater(user.id)
      'user_signup_verify'
    )
    user = App.User.current()
    @verifyLater(user.id) if user?

  verifyLater: (userId) =>
    delay = =>
      @verify(userId)
    @delay(delay, 5000, 'user_signup_verify_dialog')

  verify: (userId) ->
    return if !userId
    return if !App.User.exists(userId)
    user = App.User.find(userId)
    return if user.source isnt 'signup'
    return if user.verified is true
    currentTime = new Date().getTime()
    createdAt = Date.parse(user.created_at)
    diff = currentTime - createdAt
    max = 1000 * 60 * 30 # show message if account is older then 30 minutes
    return if diff < max
    new Modal(user: user)

class Modal extends App.ControllerModal
  backdrop: false
  keyboard: false
  head: 'Account not verified'
  small: true
  buttonClose: false
  buttonCancel: false
  buttonSubmit: 'Resend verification email'

  constructor: ->
    super

  content: =>
    if !@sent
      return App.i18n.translateContent('Your account has not been verified. Please click the link in the verification email.')
    content = App.i18n.translateContent('We\'ve sent an email to _%s_. Click the link in the email to verify your account.', @user.email)
    content += '<br><br>'
    content += App.i18n.translateContent('If you don\'t see the email, check other places it might be, like your junk, spam, social, or other folders.')
    content

  onSubmit: =>
    @ajax(
      id:          'email_verify_send'
      type:        'POST'
      url:         @apiPath + '/users/email_verify_send'
      data:        JSON.stringify(email: @user.email)
      processData: true
      success:     @success
      error:       @error
    )

  success: (data) =>
    @sent = true
    @update()

    # if in developer mode, redirect to verify
    if data.token && @Config.get('developer_mode') is true
      redirect = =>
        @close()
        @navigate "#email_verify/#{data.token}"
      App.Delay.set(redirect, 4000)

  error: =>
    @contentInline = App.i18n.translateContent('Unable to send verify email.')
    @update()

App.Config.set('user_signup', Widget, 'Widgets')
  prepend_before_action :authentication_check, except: %i[create password_reset_send password_reset_verify image]
    if !user
    #if user.verified == true
    #  render json: { error: 'Already verified!' }, status: :unprocessable_entity
    #  return
    #end

      return ["Can\'t update password, it must be at least %s characters long!", Setting.get('password_min_size')]
      return ["Can't update password, it must contain at least 1 digit!"]
      return ["Can't update password, it must contain at least 2 lowercase and 2 uppercase characters!"]
    user.update!(password: password)
      PasswordHash.verified?(user.password, password)
      value: 'some-pass',
      value: 'some-pass',
    watch_for_disappear(
      css:     '.signup',
      timeout: 10,
    )

    match(
      css:       '.user-menu .user a',
      value:     signup_user_email,
      attribute: 'title',
    )

    # check email verify
    location(url: "#{browser_url}#email_verify/not_existing")
      value: 'Unable to verify email',
    logout()
    login(
      username: signup_user_email,
      password: 'some-pass',
      url:      "#{browser_url}#email_verify/not_existing2",
    )
    watch_for(
      css:   '#content',
      value: 'Unable to verify email',
    )
    execute(
      js: 'App.Event.trigger("user_signup_verify", App.Session.get())',
    )
    modal_ready()
    click(css: '.modal .js-submit')
    execute(
      js: 'App.Auth.logout()',
    )
    sleep 6
      css: '#login',
    )
    login(
      username: signup_user_email,
      password: 'some-pass',
    )
    watch_for(
      css:   '#content',
      value: 'Your email address has been verified',
    modal_disappear()
    sleep 2
      value: 'some-pass',

Fixed code:
      success: (data, status, xhr) =>
        App.Auth.loginCheck()
        @navigate '#'

        @notify
          type:      'success'
          msg:       App.i18n.translateContent('Woo hoo! Your email address has been verified!')
          removeAll: true
          timeout: 2000

      error: (data, status, xhr) =>
        @navigate '#'

        @notify
          type:      'error'
          msg:       App.i18n.translateContent('Unable to verify email. Please contact your administrator.')
          removeAll: true
    'click .js-submitResend': 'resend'
        @html App.view('signup/verify')(
          email: @params.email
        if _.isArray(details.error)
          @form.showAlert( App.i18n.translateInline( details.error[0], details.error[1] ) )
        else
          @form.showAlert(details.error_human || details.error || 'Unable to update object!')
  resend: (e) =>
    e.preventDefault()
    @formDisable(e)
    @resendParams = @formParam(e.target)

    @ajax(
      id:          'email_verify_send'
      type:        'POST'
      url:         @apiPath + '/users/email_verify_send'
      data:        JSON.stringify(email: @resendParams.email)
      processData: true
      success: (data, status, xhr) =>
        @formEnable(e)
        # add notify
        @notify
          type:      'success'
          msg:       App.i18n.translateContent('Email sent to "%s". Please verify your email address.', @params.email)
          removeAll: true
        if data.token && @Config.get('developer_mode') is true
          @navigate "#email_verify/#{data.token}"
      error: @error
    )
    verified: true
    source: true
  resendVerificationEmail: (user) =>
    @ajax(
      id:          'email_verify_send'
      type:        'POST'
      url:         @apiPath + '/users/email_verify_send'
      data:        JSON.stringify(email: user.email)
      processData: true
      success: (data, status, xhr) =>
        @notify
          type:      'success'
          msg:       App.i18n.translateContent('Email sent to "%s". Please let the user verify his email address.', user.email)
          removeAll: true
      error: (data, status, xhr) =>
        @notify
          type:      'error'
          msg:       App.i18n.translateContent('Failed to sent Email "%s". Please contact an administrator.', user.email)
          removeAll: true
    )

      if user.verified isnt true && user.source is 'signup'
        actions.push({
          name:     'resend_verification_email'
          title:    'Resend verification email'
          callback: @resendVerificationEmail
        })

<div class="signup fullscreen">
  <div class="fullscreen-center">
    <div class="hero-unit fullscreen-body">
      <h1><%- @T('Registration successful!') %></h1>
      <p><%- @T('Thanks for joining. Email sent to "%s".', @email) %></p>
      <p><%- @T('Please click the link in the verification email.') %> <%- @T('If you don\'t see the email, check other places it might be, like your junk, spam, social, or other folders.') %></p>
      <form>
        <input type="hidden" name="email" value="<%= @email %>">
        <div class="form-controls">
          <a class="btn btn--text btn--subtle js-cancel" href="#login"><%- @T('Go Back') %></a>
          <button class="btn btn--primary js-submitResend align-right"><%- @T('Resend verification email') %></button>
        </div>
      </form>
    </div>
  </div>
</div>
  prepend_before_action :authentication_check, except: %i[create password_reset_send password_reset_verify image email_verify email_verify_send]
      # check password policy
      if clean_params[:password].present?
        result = password_policy(clean_params[:password])
        if result != true
          render json: { error: result }, status: :unprocessable_entity
          return
        end
      end

    current_user_set(user)

    if !user || user.verified == true
      return ['Invalid password, it must be at least %s characters long!', Setting.get('password_min_size')]
      return ['Invalid password, it must contain at least 1 digit!']
      return ['Invalid password, it must contain at least 2 lowercase and 2 uppercase characters!']
    user.update!(password: password, verified: true)
      password_verified = PasswordHash.verified?(user.password, password)

      raise Exceptions::NotAuthorized, 'Please verify your account before you can login!' if !user.verified && user.source == 'signup' && password_verified

      password_verified
      value: 'some-pass-123',
      value: 'some-pass-123',
      value: 'Registration successful!',
    # auto login via token trick in dev mode
    click(css: '.signup .js-submitResend')
      css:   '.content.active',
      value: 'Welcome!',
      value: 'some-pass-123',