Skip to content

Commit bc7c0b5

Browse files
committed
prevent users from unknowingly using bad regexps that can compromise security (http://homakov.blogspot.co.uk/2012/05/saferweb-injects-in-various-ruby.html)
1 parent f278b06 commit bc7c0b5

File tree

6 files changed

+67
-18
lines changed

6 files changed

+67
-18
lines changed

activemodel/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@
3737

3838
* Trim down Active Model API by removing `valid?` and `errors.full_messages` *José Valim*
3939

40+
* When `^` or `$` are used in the regular expression provided to `validates_format_of` and the :multiline option is not set to true, an exception will be raised. This is to prevent security vulnerabilities when using `validates_format_of`. The problem is described in detail in the Rails security guide.
41+
42+
## Rails 3.2.6 (Jun 12, 2012) ##
43+
44+
* No changes.
4045

4146
## Rails 3.2.5 (Jun 1, 2012) ##
4247

activemodel/lib/active_model/validations/format.rb

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,21 @@ def option_call(record, name)
3232
def record_error(record, attribute, name, value)
3333
record.errors.add(attribute, :invalid, options.except(name).merge!(:value => value))
3434
end
35-
35+
36+
def regexp_using_multiline_anchors?(regexp)
37+
regexp.source.start_with?("^") ||
38+
(regexp.source.end_with?("$") && !regexp.source.end_with?("\\$"))
39+
end
40+
3641
def check_options_validity(options, name)
3742
option = options[name]
3843
if option && !option.is_a?(Regexp) && !option.respond_to?(:call)
3944
raise ArgumentError, "A regular expression or a proc or lambda must be supplied as :#{name}"
45+
elsif option && option.is_a?(Regexp) &&
46+
regexp_using_multiline_anchors?(option) && options[:multiline] != true
47+
raise ArgumentError, "The provided regular expression is using multiline anchors (^ or $), " \
48+
"which may present a security risk. Did you mean to use \\A and \\z, or forgot to add the " \
49+
":multiline => true option?"
4050
end
4151
end
4252
end
@@ -47,7 +57,7 @@ module HelperMethods
4757
# attribute matches the regular expression:
4858
#
4959
# class Person < ActiveRecord::Base
50-
# validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, :on => :create
60+
# validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :on => :create
5161
# end
5262
#
5363
# Alternatively, you can require that the specified attribute does _not_
@@ -63,12 +73,16 @@ module HelperMethods
6373
# class Person < ActiveRecord::Base
6474
# # Admin can have number as a first letter in their screen name
6575
# validates_format_of :screen_name,
66-
# :with => lambda{ |person| person.admin? ? /\A[a-z0-9][a-z0-9_\-]*\Z/i : /\A[a-z][a-z0-9_\-]*\Z/i }
76+
# :with => lambda{ |person| person.admin? ? /\A[a-z0-9][a-z0-9_\-]*\z/i : /\A[a-z][a-z0-9_\-]*\z/i }
6777
# end
6878
#
6979
# Note: use <tt>\A</tt> and <tt>\Z</tt> to match the start and end of the
7080
# string, <tt>^</tt> and <tt>$</tt> match the start/end of a line.
7181
#
82+
# Due to frequent misuse of <tt>^</tt> and <tt>$</tt>, you need to pass the
83+
# :multiline => true option in case you use any of these two anchors in the provided
84+
# regular expression. In most cases, you should be using <tt>\A</tt> and <tt>\z</tt>.
85+
#
7286
# You must pass either <tt>:with</tt> or <tt>:without</tt> as an option.
7387
# In addition, both must be a regular expression or a proc or lambda, or
7488
# else an exception will be raised.
@@ -98,6 +112,9 @@ module HelperMethods
98112
# method, proc or string should return or evaluate to a true or false value.
99113
# * <tt>:strict</tt> - Specifies whether validation should be strict.
100114
# See <tt>ActiveModel::Validation#validates!</tt> for more information.
115+
# * <tt>:multiline</tt> - Set to true if your regular expression contains
116+
# anchors that match the beginning or end of lines as opposed to the
117+
# beginning or end of the string. These anchors are <tt>^</tt> and <tt>$</tt>.
101118
def validates_format_of(*attr_names)
102119
validates_with FormatValidator, _merge_attributes(attr_names)
103120
end

activemodel/test/cases/validations/format_validation_test.rb

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def teardown
1111
end
1212

1313
def test_validate_format
14-
Topic.validates_format_of(:title, :content, :with => /^Validation\smacros \w+!$/, :message => "is bad data")
14+
Topic.validates_format_of(:title, :content, :with => /\AValidation\smacros \w+!\z/, :message => "is bad data")
1515

1616
t = Topic.new("title" => "i'm incorrect", "content" => "Validation macros rule!")
1717
assert t.invalid?, "Shouldn't be valid"
@@ -27,7 +27,7 @@ def test_validate_format
2727
end
2828

2929
def test_validate_format_with_allow_blank
30-
Topic.validates_format_of(:title, :with => /^Validation\smacros \w+!$/, :allow_blank => true)
30+
Topic.validates_format_of(:title, :with => /\AValidation\smacros \w+!\z/, :allow_blank => true)
3131
assert Topic.new("title" => "Shouldn't be valid").invalid?
3232
assert Topic.new("title" => "").valid?
3333
assert Topic.new("title" => nil).valid?
@@ -36,7 +36,7 @@ def test_validate_format_with_allow_blank
3636

3737
# testing ticket #3142
3838
def test_validate_format_numeric
39-
Topic.validates_format_of(:title, :content, :with => /^[1-9][0-9]*$/, :message => "is bad data")
39+
Topic.validates_format_of(:title, :content, :with => /\A[1-9][0-9]*\z/, :message => "is bad data")
4040

4141
t = Topic.new("title" => "72x", "content" => "6789")
4242
assert t.invalid?, "Shouldn't be valid"
@@ -63,11 +63,21 @@ def test_validate_format_numeric
6363
end
6464

6565
def test_validate_format_with_formatted_message
66-
Topic.validates_format_of(:title, :with => /^Valid Title$/, :message => "can't be %{value}")
66+
Topic.validates_format_of(:title, :with => /\AValid Title\z/, :message => "can't be %{value}")
6767
t = Topic.new(:title => 'Invalid title')
6868
assert t.invalid?
6969
assert_equal ["can't be Invalid title"], t.errors[:title]
7070
end
71+
72+
def test_validate_format_of_with_multiline_regexp_should_raise_error
73+
assert_raise(ArgumentError) { Topic.validates_format_of(:title, :with => /^Valid Title$/) }
74+
end
75+
76+
def test_validate_format_of_with_multiline_regexp_and_option
77+
assert_nothing_raised(ArgumentError) do
78+
Topic.validates_format_of(:title, :with => /^Valid Title$/, :multiline => true)
79+
end
80+
end
7181

7282
def test_validate_format_with_not_option
7383
Topic.validates_format_of(:title, :without => /foo/, :message => "should not contain foo")

activemodel/test/cases/validations/i18n_validation_test.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ def test_errors_full_messages_uses_format
141141

142142
COMMON_CASES.each do |name, validation_options, generate_message_options|
143143
test "validates_format_of on generated message #{name}" do
144-
Person.validates_format_of :title, validation_options.merge(:with => /^[1-9][0-9]*$/)
144+
Person.validates_format_of :title, validation_options.merge(:with => /\A[1-9][0-9]*\z/)
145145
@person.title = '72x'
146146
@person.errors.expects(:generate_message).with(:title, :invalid, generate_message_options.merge(:value => '72x'))
147147
@person.valid?
@@ -291,7 +291,7 @@ def self.set_expectations_for_validation(validation, error_type, &block_that_set
291291
# validates_format_of w/o mocha
292292

293293
set_expectations_for_validation "validates_format_of", :invalid do |person, options_to_merge|
294-
Person.validates_format_of :title, options_to_merge.merge(:with => /^[1-9][0-9]*$/)
294+
Person.validates_format_of :title, options_to_merge.merge(:with => /\A[1-9][0-9]*\z/)
295295
end
296296

297297
# validates_inclusion_of w/o mocha

guides/source/active_model_basics.textile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ class Person
187187
attr_accessor :name, :email, :token
188188

189189
validates :name, :presence => true
190-
validates_format_of :email, :with => /^([^\s]+)((?:[-a-z0-9]\.)[a-z]{2,})$/i
190+
validates_format_of :email, :with => /\A([^\s]+)((?:[-a-z0-9]\.)[a-z]{2,})\z/i
191191
validates! :token, :presence => true
192192

193193
end

guides/source/security.textile

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -588,26 +588,43 @@ h4. Regular Expressions
588588

589589
INFO: _A common pitfall in Ruby's regular expressions is to match the string's beginning and end by ^ and $, instead of \A and \z._
590590

591-
Ruby uses a slightly different approach than many other languages to match the end and the beginning of a string. That is why even many Ruby and Rails books make this wrong. So how is this a security threat? Imagine you have a File model and you validate the file name by a regular expression like this:
591+
Ruby uses a slightly different approach than many other languages to match the end and the beginning of a string. That is why even many Ruby and Rails books make this wrong. So how is this a security threat? Say you wanted to loosely validate a URL field and you used a simple regular expression like this:
592592

593593
<ruby>
594-
class File < ActiveRecord::Base
595-
validates :name, :format => /^[\w\.\-\<plus>]<plus>$/
596-
end
594+
/^https?:\/\/[^\n]+$/i
597595
</ruby>
598596

599-
This means, upon saving, the model will validate the file name to consist only of alphanumeric characters, dots, + and -. And the programmer added ^ and $ so that file name will contain these characters from the beginning to the end of the string. However, _(highlight)in Ruby ^ and $ matches the *line* beginning and line end_. And thus a file name like this passes the filter without problems:
597+
This may work fine in some languages. However, _(highlight)in Ruby ^ and $ match the *line* beginning and line end_. And thus a URL like this passes the filter without problems:
600598

601599
<plain>
602-
file.txt%0A<script>alert('hello')</script>
600+
javascript:exploit_code();/*
601+
http://hi.com
602+
*/
603603
</plain>
604604

605-
Whereas %0A is a line feed in URL encoding, so Rails automatically converts it to "file.txt\n&lt;script&gt;alert('hello')&lt;/script&gt;". This file name passes the filter because the regular expression matches – up to the line end, the rest does not matter. The correct expression should read:
605+
This URL passes the filter because the regular expression matches – the second line, the rest does not matter. Now imagine we had a view that showed the URL like this:
606+
607+
<ruby>
608+
link_to "Homepage", @user.homepage
609+
</ruby>
610+
611+
The link looks innocent to visitors, but when it's clicked, it will execute the javascript function "exploit_code" or any other javascript the attacker provides.
612+
613+
To fix the regular expression, \A and \z should be used instead of ^ and $, like so:
606614

607615
<ruby>
608-
/\A[\w\.\-\<plus>]<plus>\z/
616+
/\Ahttps?:\/\/[^\n]+\z/i
609617
</ruby>
610618

619+
Since this is a frequent mistake, the format validator (validates_format_of) now raises an exception if the provided regular expression starts with ^ or ends with $. If you do need to use ^ and $ instead of \A and \z (which is rare), you can set the :multiline option to true, like so:
620+
621+
<ruby>
622+
# content should include a line "Meanwhile" anywhere in the string
623+
validates :content, :format => { :with => /^Meanwhile$/, :multiline => true }
624+
</ruby>
625+
626+
Note that this only protects you against the most common mistake when using the format validator - you always need to keep in mind that ^ and $ match the *line* beginning and line end in Ruby, and not the beginning and end of a string.
627+
611628
h4. Privilege Escalation
612629

613630
WARNING: _Changing a single parameter may give the user unauthorized access. Remember that every parameter may be changed, no matter how much you hide or obfuscate it._

0 commit comments

Comments
 (0)