Railsmagazine60x60 Protecting Your Application From Impostors

by Gavin Morrice

Issue: Winter Jam

published in December 2009

Gavin morrice

Gavin Morrice is a freelance Ruby developer/consultant. He specializes in developing web applications using the Ruby on Rails framework. Gavin is a BDD (behavior driven development) enthusiast, which means the code he writes caters precisely to his clients specifications and business goals.

If your application is open to the public, where people can freely create a user account and start adding content or posting comments or forum entries, it's important that you maintain some control over what they can and cannot do.

Leaving usernames like "admin", "webmaster" or your domain name up for grabs could be pretty damaging, especially if they are misused by a malicious user. Imagine if a member of the public was free to leave offensive comments using your name or "admin".

By taking a few basic precautions, you can prevent this sort of abuse without having to continuously monitor your app. Here are a couple of ideas to get you started.

The simplest option is to use validates_exclusion_of to check if a new user's username appears in a list of prohibited usernames:

class User < ActiveRecord::Base

 

validates_exclusion_of :username,

:in => %w{ admin webmaster mydomain.com },

:on => :create,

:message => "is not available"

 

end

If the user's username appears in the array of names passed to the :in option, then the record is invalid. The error message here reads simply "is not available". You could go with something more authoritarian like "is restricted!" but that's not really necessary.

One problem with this approach is that validates_exclusion_of is case-sensitive. If a user signs up with the username "Admin" or "ADMIN", then their username will not be invalid. You could get around this with a before_validation callback to format the username before it's validated. But that means the original case will not be preserved.

class User < ActiveRecord::Base

 

  before_validation :downcase_username

 

  validates_exclusion_of :username,

    :in => %w{ admin administrator superuser mydomain.com },

    :on => :create,

    :message => "is not available"

 

private

 

  def downcase_username

    self.username.downcase!

  end

 

end

Another problem with this approach is that validates_exclusion_of only checks for exact values so "admin1" or "admin:" would be considered valid even though they are still misleading.

For a more effective validation, you can write your own custom method and call it using validate or validate_on_create.

class User < ActiveRecord::Base

 

  validate_on_create :check_username_is_allowed

 

private

 

  def check_username_is_allowed

    %w{ admin webmaster mydomain }.each do |restricted_value|

      errors.add(:username, "is not available") if username =~ /#{restricted_value}/i unless errors.on(:username)

    end

  end

 

end

In this example, the private method check_username_is_allowed will be called before any new user records are created. The user's username is scanned for each of the restricted values and if the restricted value appears anyplace within the username an error message "is not available" is added. This means we can now restrict usernames like "admin1" and "administrator" as well as "Admin:" and "Administrator". You'll notice there's an 'i' after the regular expression. This ensures the regexp will be case insensitive. You'll also notice the 'unless' clause. This will ensure an error will only be added if there are not already errors on the username; No point in bombarding the user with several error messages on the same attribute.

We can expand this a little further by moving the array of restricted values to a constant:

class User < ActiveRecord::Base

 

  RESTRICTED_NAMES = %w{

    admin

    webmaster

    mydomain.com

    superuser

  }

 

  validate_on_create :check_username_is_allowed

 

private

 

  def check_username_is_allowed

    RESTRICTED_NAMES.each do |restricted|

      errors.add(:username, "is not available") if username =~ /#{restricted}/i unless errors.on(:username)

    end

  end

 

end

 

Once you start thinking of the reserved or offensive words you want to prohibit you'll probably surprise yourself with how many you can think of. Moving these values to an constant will keep your check_username_is_allowed method small and legible regardless of how many restricted names you add to the list.

Finally, you may decide to extend this idea across more than one model. For example, you may also have a comment model that you want to add the same sort of validation to. In this case, to keep your models DRY, you should move this code to a module and include it where required.

# in lib/username_validations

module UsernameValidations

 

  # this method is invoked when the module is included into another module or class

  def self.included(my_class)

    my_class.send(:validate_on_create, :check_username_is_allowed)

  end

 

  RESTRICTED_NAMES = %w{

    admin

    webmaster

    mydomain.com

    superuser

  }

 

private

 

  def check_username_is_allowed

    RESTRICTED_NAMES.each do |restricted|

      errors.add(:username, "is not available") if username =~ /#{restricted}/i unless errors.on(:username)

    end

  end

 

end

and, in your models:

class User < ActiveRecord::Base

  include UsernameValidations

end

class Comment < ActiveRecord::Base

  include UsernameValidations

end

By writing some custom validations like this you can prevent people from adding inappropriate content to your site, saving you from having to filter through every new post/comment etc. checking them. You can also employ the same method to help prevent spam on your site. Just add the usual words, viagra, weight-loss etc. to the list of restricted values.