brakeman 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +3 -1
- data/lib/checks/base_check.rb +4 -2
- data/lib/checks/check_cross_site_scripting.rb +74 -47
- data/lib/checks/check_file_access.rb +3 -1
- data/lib/checks/check_forgery_setting.rb +2 -2
- data/lib/processors/erubis_template_processor.rb +4 -2
- data/lib/processors/output_processor.rb +1 -1
- data/lib/processors/template_processor.rb +4 -0
- data/lib/report.rb +3 -1
- data/lib/scanner.rb +10 -6
- data/lib/tracker.rb +1 -0
- data/lib/version.rb +1 -1
- metadata +4 -4
data/README.md
CHANGED
@@ -4,6 +4,8 @@ Brakeman is a static analysis tool which checks Ruby on Rails applications for s
|
|
4
4
|
|
5
5
|
It targets Rails versions > 2.0 with experimental support for Rails 3.x
|
6
6
|
|
7
|
+
There is also a [plugin available](https://github.com/presidentbeef/brakeman-jenkins-plugin) for Jenkins/Hudson.
|
8
|
+
|
7
9
|
# Installation
|
8
10
|
|
9
11
|
Using RubyGems:
|
@@ -19,7 +21,7 @@ From source:
|
|
19
21
|
|
20
22
|
brakeman [app_path]
|
21
23
|
|
22
|
-
It is simplest to run
|
24
|
+
It is simplest to run Brakeman from the root directory of the Rails application. A path may also be supplied.
|
23
25
|
|
24
26
|
# Options
|
25
27
|
|
data/lib/checks/base_check.rb
CHANGED
@@ -172,7 +172,9 @@ class BaseCheck < SexpProcessor
|
|
172
172
|
#(either :params or :cookies) and the second element is the matching
|
173
173
|
#expression
|
174
174
|
def has_immediate_user_input? exp
|
175
|
-
if
|
175
|
+
if exp.nil?
|
176
|
+
return false
|
177
|
+
elsif params? exp
|
176
178
|
return :params, exp
|
177
179
|
elsif cookies? exp
|
178
180
|
return :cookies, exp
|
@@ -180,7 +182,7 @@ class BaseCheck < SexpProcessor
|
|
180
182
|
if sexp? exp[1]
|
181
183
|
if ALL_PARAMETERS.include? exp[1] or params? exp[1]
|
182
184
|
return :params, exp
|
183
|
-
elsif exp[1] == COOKIES
|
185
|
+
elsif exp[1] == COOKIES or cookies? exp[1]
|
184
186
|
return :cookies, exp
|
185
187
|
else
|
186
188
|
false
|
@@ -22,8 +22,12 @@ class CheckCrossSiteScripting < BaseCheck
|
|
22
22
|
:fields_for, :label, :text_area, :text_field, :hidden_field, :check_box,
|
23
23
|
:field_field])
|
24
24
|
|
25
|
+
#Model methods which are known to be harmless
|
25
26
|
IGNORE_MODEL_METHODS = Set.new([:average, :count, :maximum, :minimum, :sum])
|
26
27
|
|
28
|
+
#Methods known to not escape their input
|
29
|
+
KNOWN_DANGEROUS = Set.new([:auto_link, :truncate, :concat])
|
30
|
+
|
27
31
|
MODEL_METHODS = Set.new([:all, :find, :first, :last, :new])
|
28
32
|
|
29
33
|
IGNORE_LIKE = /^link_to_|(_path|_tag|_url)$/
|
@@ -50,47 +54,7 @@ class CheckCrossSiteScripting < BaseCheck
|
|
50
54
|
@current_template = template
|
51
55
|
|
52
56
|
template[:outputs].each do |out|
|
53
|
-
|
54
|
-
if type and not duplicate? out
|
55
|
-
add_result out
|
56
|
-
case type
|
57
|
-
when :params
|
58
|
-
message = "Unescaped parameter value"
|
59
|
-
when :cookies
|
60
|
-
message = "Unescaped cookie value"
|
61
|
-
else
|
62
|
-
message = "Unescaped user input value"
|
63
|
-
end
|
64
|
-
|
65
|
-
warn :template => @current_template,
|
66
|
-
:warning_type => "Cross Site Scripting",
|
67
|
-
:message => message,
|
68
|
-
:line => match.line,
|
69
|
-
:code => match,
|
70
|
-
:confidence => CONFIDENCE[:high]
|
71
|
-
|
72
|
-
elsif not OPTIONS[:ignore_model_output] and match = has_immediate_model?(out[1])
|
73
|
-
method = match[2]
|
74
|
-
|
75
|
-
unless duplicate? out or IGNORE_MODEL_METHODS.include? method
|
76
|
-
add_result out
|
77
|
-
|
78
|
-
if MODEL_METHODS.include? method or method.to_s =~ /^find_by/
|
79
|
-
confidence = CONFIDENCE[:high]
|
80
|
-
else
|
81
|
-
confidence = CONFIDENCE[:med]
|
82
|
-
end
|
83
|
-
|
84
|
-
code = find_chain out, match
|
85
|
-
warn :template => @current_template,
|
86
|
-
:warning_type => "Cross Site Scripting",
|
87
|
-
:message => "Unescaped model attribute",
|
88
|
-
:line => code.line,
|
89
|
-
:code => code,
|
90
|
-
:confidence => confidence
|
91
|
-
end
|
92
|
-
|
93
|
-
else
|
57
|
+
unless check_for_immediate_xss out
|
94
58
|
@matched = false
|
95
59
|
@mark = false
|
96
60
|
process out
|
@@ -99,6 +63,59 @@ class CheckCrossSiteScripting < BaseCheck
|
|
99
63
|
end
|
100
64
|
end
|
101
65
|
|
66
|
+
def check_for_immediate_xss exp
|
67
|
+
if exp[0] == :output
|
68
|
+
out = exp[1]
|
69
|
+
elsif exp[0] == :escaped_output and raw_call? exp
|
70
|
+
out = exp[1][3][1]
|
71
|
+
end
|
72
|
+
|
73
|
+
type, match = has_immediate_user_input? out
|
74
|
+
|
75
|
+
if type and not duplicate? exp
|
76
|
+
add_result exp
|
77
|
+
case type
|
78
|
+
when :params
|
79
|
+
message = "Unescaped parameter value"
|
80
|
+
when :cookies
|
81
|
+
message = "Unescaped cookie value"
|
82
|
+
else
|
83
|
+
message = "Unescaped user input value"
|
84
|
+
end
|
85
|
+
|
86
|
+
warn :template => @current_template,
|
87
|
+
:warning_type => "Cross Site Scripting",
|
88
|
+
:message => message,
|
89
|
+
:line => match.line,
|
90
|
+
:code => match,
|
91
|
+
:confidence => CONFIDENCE[:high]
|
92
|
+
|
93
|
+
elsif not OPTIONS[:ignore_model_output] and match = has_immediate_model?(out)
|
94
|
+
method = match[2]
|
95
|
+
|
96
|
+
unless duplicate? out or IGNORE_MODEL_METHODS.include? method
|
97
|
+
add_result out
|
98
|
+
|
99
|
+
if MODEL_METHODS.include? method or method.to_s =~ /^find_by/
|
100
|
+
confidence = CONFIDENCE[:high]
|
101
|
+
else
|
102
|
+
confidence = CONFIDENCE[:med]
|
103
|
+
end
|
104
|
+
|
105
|
+
code = find_chain out, match
|
106
|
+
warn :template => @current_template,
|
107
|
+
:warning_type => "Cross Site Scripting",
|
108
|
+
:message => "Unescaped model attribute",
|
109
|
+
:line => code.line,
|
110
|
+
:code => code,
|
111
|
+
:confidence => confidence
|
112
|
+
end
|
113
|
+
|
114
|
+
else
|
115
|
+
false
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
102
119
|
#Process an output Sexp
|
103
120
|
def process_output exp
|
104
121
|
process exp[1].dup
|
@@ -107,11 +124,12 @@ class CheckCrossSiteScripting < BaseCheck
|
|
107
124
|
#Look for calls to raw()
|
108
125
|
#Otherwise, ignore
|
109
126
|
def process_escaped_output exp
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
127
|
+
unless check_for_immediate_xss exp
|
128
|
+
if raw_call? exp
|
129
|
+
process exp[1][3][1]
|
130
|
+
end
|
114
131
|
end
|
132
|
+
exp
|
115
133
|
end
|
116
134
|
|
117
135
|
#Check a call for user input
|
@@ -137,12 +155,18 @@ class CheckCrossSiteScripting < BaseCheck
|
|
137
155
|
if message and not duplicate? exp
|
138
156
|
add_result exp
|
139
157
|
|
158
|
+
if exp[1].nil? and KNOWN_DANGEROUS.include? exp[2]
|
159
|
+
confidence = CONFIDENCE[:high]
|
160
|
+
else
|
161
|
+
confidence = CONFIDENCE[:low]
|
162
|
+
end
|
163
|
+
|
140
164
|
warn :template => @current_template,
|
141
165
|
:warning_type => "Cross Site Scripting",
|
142
166
|
:message => message,
|
143
167
|
:line => exp.line,
|
144
168
|
:code => exp,
|
145
|
-
:confidence =>
|
169
|
+
:confidence => confidence
|
146
170
|
end
|
147
171
|
|
148
172
|
@mark = @matched = false
|
@@ -221,6 +245,10 @@ class CheckCrossSiteScripting < BaseCheck
|
|
221
245
|
end
|
222
246
|
exp
|
223
247
|
end
|
248
|
+
|
249
|
+
def raw_call? exp
|
250
|
+
exp[1].node_type == :call and exp[1][2] == :raw
|
251
|
+
end
|
224
252
|
end
|
225
253
|
|
226
254
|
#This _only_ checks calls to link_to
|
@@ -316,7 +344,6 @@ class CheckLinkTo < CheckCrossSiteScripting
|
|
316
344
|
exp
|
317
345
|
end
|
318
346
|
|
319
|
-
|
320
347
|
def actually_process_call exp
|
321
348
|
return if @matched
|
322
349
|
|
@@ -6,7 +6,9 @@ class CheckFileAccess < BaseCheck
|
|
6
6
|
Checks.add self
|
7
7
|
|
8
8
|
def run_check
|
9
|
-
methods = tracker.find_call [
|
9
|
+
methods = tracker.find_call [:Dir, :File, :IO, :Kernel, :"Net::FTP", :"Net::HTTP", :PStore, :Pathname, :Shell, :YAML], [:[], :chdir, :chroot, :delete, :entries, :foreach, :glob, :install, :lchmod, :lchown, :link, :load, :load_file, :makedirs, :move, :new, :open, :read, :read_lines, :rename, :rmdir, :safe_unlink, :symlink, :syscopy, :sysopen, :truncate, :unlink]
|
10
|
+
|
11
|
+
methods.concat tracker.find_call [], [:load]
|
10
12
|
|
11
13
|
methods.concat tracker.find_call(:FileUtils, nil)
|
12
14
|
|
@@ -28,14 +28,14 @@ class CheckForgerySetting < BaseCheck
|
|
28
28
|
|
29
29
|
warn :controller => :ApplicationController,
|
30
30
|
:warning_type => "Cross-Site Request Forgery",
|
31
|
-
:message => "CSRF protection is flawed in #{tracker.config[:rails_version]} (CVE-2011-0447). Upgrade to 2.3.11 or apply patches",
|
31
|
+
:message => "CSRF protection is flawed in unpatched versions of Rails #{tracker.config[:rails_version]} (CVE-2011-0447). Upgrade to 2.3.11 or apply patches as needed",
|
32
32
|
:confidence => CONFIDENCE[:high]
|
33
33
|
|
34
34
|
elsif version_between? "3.0.0", "3.0.3"
|
35
35
|
|
36
36
|
warn :controller => :ApplicationController,
|
37
37
|
:warning_type => "Cross-Site Request Forgery",
|
38
|
-
:message => "CSRF protection is flawed in #{tracker.config[:rails_version]} (CVE-2011-0447). Upgrade to 3.0.4",
|
38
|
+
:message => "CSRF protection is flawed in unpatched versions of Rails #{tracker.config[:rails_version]} (CVE-2011-0447). Upgrade to 3.0.4 or apply patches as needed",
|
39
39
|
:confidence => CONFIDENCE[:high]
|
40
40
|
end
|
41
41
|
end
|
@@ -61,7 +61,9 @@ class ErubisTemplateProcessor < TemplateProcessor
|
|
61
61
|
block
|
62
62
|
end
|
63
63
|
|
64
|
-
#Look for assignments to output buffer
|
64
|
+
#Look for assignments to output buffer that look like this:
|
65
|
+
# @output_buffer.append = some_output
|
66
|
+
# @output_buffer.safe_append = some_output
|
65
67
|
def process_attrasgn exp
|
66
68
|
if exp[1].node_type == :ivar and exp[1][1] == :@output_buffer
|
67
69
|
if exp[2] == :append= or exp[2] == :safe_append=
|
@@ -76,7 +78,7 @@ class ErubisTemplateProcessor < TemplateProcessor
|
|
76
78
|
s
|
77
79
|
end
|
78
80
|
else
|
79
|
-
|
81
|
+
super
|
80
82
|
end
|
81
83
|
else
|
82
84
|
super
|
data/lib/report.rb
CHANGED
@@ -274,7 +274,9 @@ class Report
|
|
274
274
|
end
|
275
275
|
|
276
276
|
res = generate_errors
|
277
|
-
|
277
|
+
if res
|
278
|
+
out << "<div onClick=\"toggle('errors_table');\"> <h2>Exceptions raised during the analysis (click to see them)</h2 ></div> <div id='errors_table' style='display:none'>" << res.to_html<< '</div>'
|
279
|
+
end
|
278
280
|
|
279
281
|
res = generate_warnings
|
280
282
|
out << "<h2>Security Warnings</h2>" << res.to_html if res
|
data/lib/scanner.rb
CHANGED
@@ -28,7 +28,7 @@ class Scanner
|
|
28
28
|
#Pass in path to the root of the Rails application
|
29
29
|
def initialize path
|
30
30
|
@path = path
|
31
|
-
@app_path = path
|
31
|
+
@app_path = File.join(path, "app")
|
32
32
|
@processor = Processor.new
|
33
33
|
end
|
34
34
|
|
@@ -82,7 +82,7 @@ class Scanner
|
|
82
82
|
begin
|
83
83
|
@processor.process_initializer(f, RubyParser.new.parse(File.read(f)))
|
84
84
|
rescue Racc::ParseError => e
|
85
|
-
tracker.error e, "could not parse #{f}"
|
85
|
+
tracker.error e, "could not parse #{f}. There is probably a typo in the file. Test it with 'ruby_parse #{f}'"
|
86
86
|
rescue Exception => e
|
87
87
|
tracker.error e.exception(e.message + "\nWhile processing #{f}"), e.backtrace
|
88
88
|
end
|
@@ -97,7 +97,7 @@ class Scanner
|
|
97
97
|
begin
|
98
98
|
@processor.process_lib RubyParser.new.parse(File.read(f)), f
|
99
99
|
rescue Racc::ParseError => e
|
100
|
-
tracker.error e, "could not parse #{f}"
|
100
|
+
tracker.error e, "could not parse #{f}. There is probably a typo in the file. Test it with 'ruby_parse #{f}'"
|
101
101
|
rescue Exception => e
|
102
102
|
tracker.error e.exception(e.message + "\nWhile processing #{f}"), e.backtrace
|
103
103
|
end
|
@@ -110,6 +110,8 @@ class Scanner
|
|
110
110
|
def process_routes
|
111
111
|
if File.exists? "#@path/config/routes.rb"
|
112
112
|
@processor.process_routes RubyParser.new.parse(File.read("#@path/config/routes.rb"))
|
113
|
+
else
|
114
|
+
warn "[Notice] No route information found"
|
113
115
|
end
|
114
116
|
end
|
115
117
|
|
@@ -121,7 +123,7 @@ class Scanner
|
|
121
123
|
begin
|
122
124
|
@processor.process_controller(RubyParser.new.parse(File.read(f)), f)
|
123
125
|
rescue Racc::ParseError => e
|
124
|
-
tracker.error e, "could not parse #{f}"
|
126
|
+
tracker.error e, "could not parse #{f}. There is probably a typo in the file. Test it with 'ruby_parse #{f}'"
|
125
127
|
rescue Exception => e
|
126
128
|
tracker.error e.exception(e.message + "\nWhile processing #{f}"), e.backtrace
|
127
129
|
end
|
@@ -224,9 +226,11 @@ class RailsXSSErubis < ::Erubis::Eruby
|
|
224
226
|
end
|
225
227
|
|
226
228
|
def add_text(src, text)
|
227
|
-
if text
|
229
|
+
if text == "\n"
|
230
|
+
src << "\n"
|
231
|
+
elsif text.include? "\n"
|
228
232
|
lines = text.split("\n")
|
229
|
-
if text.match
|
233
|
+
if text.match(/\n\z/)
|
230
234
|
lines.each do |line|
|
231
235
|
src << "@output_buffer << ('" << escape_text(line) << "'.html_safe!);\n"
|
232
236
|
end
|
data/lib/tracker.rb
CHANGED
data/lib/version.rb
CHANGED
@@ -1 +1 @@
|
|
1
|
-
Version = "0.
|
1
|
+
Version = "0.6.0"
|
metadata
CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
|
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
6
|
- 0
|
7
|
-
-
|
8
|
-
-
|
9
|
-
version: 0.
|
7
|
+
- 6
|
8
|
+
- 0
|
9
|
+
version: 0.6.0
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Justin Collins
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2011-
|
17
|
+
date: 2011-07-20 00:00:00 -07:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|