spektr 0.4.1 → 0.5.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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yaml +2 -2
- data/CHANGELOG.md +4 -0
- data/CODE_OF_CONDUCT.md +1 -72
- data/lib/spektr/app.rb +2 -48
- data/lib/spektr/checks/base.rb +85 -60
- data/lib/spektr/checks/basic_auth.rb +2 -1
- data/lib/spektr/checks/command_injection.rb +15 -10
- data/lib/spektr/checks/content_tag_xss.rb +17 -9
- data/lib/spektr/checks/cookie_serialization.rb +1 -1
- data/lib/spektr/checks/create_with.rb +3 -3
- data/lib/spektr/checks/csrf_setting.rb +4 -18
- data/lib/spektr/checks/default_routes.rb +3 -2
- data/lib/spektr/checks/deserialize.rb +9 -13
- data/lib/spektr/checks/detailed_exceptions.rb +3 -3
- data/lib/spektr/checks/dynamic_finders.rb +2 -2
- data/lib/spektr/checks/evaluation.rb +2 -2
- data/lib/spektr/checks/file_access.rb +3 -3
- data/lib/spektr/checks/file_disclosure.rb +1 -1
- data/lib/spektr/checks/filter_skipping.rb +3 -1
- data/lib/spektr/checks/json_encoding.rb +1 -0
- data/lib/spektr/checks/json_entity_escape.rb +8 -5
- data/lib/spektr/checks/json_parsing.rb +1 -1
- data/lib/spektr/checks/link_to_href.rb +8 -6
- data/lib/spektr/checks/mass_assignment.rb +7 -7
- data/lib/spektr/checks/send.rb +2 -2
- data/lib/spektr/checks/sqli.rb +6 -10
- data/lib/spektr/checks/xss.rb +10 -7
- data/lib/spektr/extractors/calls.rb +22 -0
- data/lib/spektr/extractors/methods.rb +28 -0
- data/lib/spektr/targets/base.rb +80 -81
- data/lib/spektr/targets/controller.rb +35 -24
- data/lib/spektr/targets/routes.rb +1 -1
- data/lib/spektr/targets/view.rb +8 -9
- data/lib/spektr/version.rb +1 -1
- data/lib/spektr/warning.rb +2 -2
- data/lib/spektr.rb +8 -8
- data/spektr.gemspec +3 -2
- metadata +23 -20
- data/lib/spektr/exp/assignment.rb +0 -20
- data/lib/spektr/exp/base.rb +0 -32
- data/lib/spektr/exp/const.rb +0 -7
- data/lib/spektr/exp/definition.rb +0 -32
- data/lib/spektr/exp/ivasign.rb +0 -7
- data/lib/spektr/exp/lvasign.rb +0 -7
- data/lib/spektr/exp/send.rb +0 -135
- data/lib/spektr/exp/xstr.rb +0 -12
- data/lib/spektr/processors/base.rb +0 -87
- data/lib/spektr/processors/class_processor.rb +0 -24
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2b586f883df20f845c122f773d27dc2c63667d9ed2a69f06c3f3069ad7b29241
|
|
4
|
+
data.tar.gz: 99ae92d6f76d3cb0d26959893ee49d7351628f4813f12cfd4a606f0089e4ac16
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7247491a03096d550b1a08e4eab89cfcb6e61526057ec6a6726d45981b404feb3898392f733bcb94de78711c883eec3b6d65f95e6ae10a2480574c9f928b1f1a
|
|
7
|
+
data.tar.gz: 97030d2527e8c3d8aa1ee9ec9e0afbe4bebe5092189ff8511e4dc86744fa64ba0926f88173d33dabe433c7bbd64db3d2756aa323f79fe383edff7af2474bdc6c
|
data/.github/workflows/ci.yaml
CHANGED
|
@@ -18,11 +18,11 @@ jobs:
|
|
|
18
18
|
strategy:
|
|
19
19
|
fail-fast: false
|
|
20
20
|
matrix:
|
|
21
|
-
ruby: [2.
|
|
21
|
+
ruby: [3.2, 3.3, 3.4]
|
|
22
22
|
steps:
|
|
23
23
|
- uses: actions/checkout@v2
|
|
24
24
|
- name: Set up Ruby
|
|
25
|
-
uses: ruby/setup-ruby@
|
|
25
|
+
uses: ruby/setup-ruby@v1
|
|
26
26
|
with:
|
|
27
27
|
bundler-cache: true
|
|
28
28
|
ruby-version: ${{ matrix.ruby }}
|
data/CHANGELOG.md
CHANGED
data/CODE_OF_CONDUCT.md
CHANGED
|
@@ -1,74 +1,3 @@
|
|
|
1
1
|
# Contributor Covenant Code of Conduct
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
In the interest of fostering an open and welcoming environment, we as
|
|
6
|
-
contributors and maintainers pledge to making participation in our project and
|
|
7
|
-
our community a harassment-free experience for everyone, regardless of age, body
|
|
8
|
-
size, disability, ethnicity, gender identity and expression, level of experience,
|
|
9
|
-
nationality, personal appearance, race, religion, or sexual identity and
|
|
10
|
-
orientation.
|
|
11
|
-
|
|
12
|
-
## Our Standards
|
|
13
|
-
|
|
14
|
-
Examples of behavior that contributes to creating a positive environment
|
|
15
|
-
include:
|
|
16
|
-
|
|
17
|
-
* Using welcoming and inclusive language
|
|
18
|
-
* Being respectful of differing viewpoints and experiences
|
|
19
|
-
* Gracefully accepting constructive criticism
|
|
20
|
-
* Focusing on what is best for the community
|
|
21
|
-
* Showing empathy towards other community members
|
|
22
|
-
|
|
23
|
-
Examples of unacceptable behavior by participants include:
|
|
24
|
-
|
|
25
|
-
* The use of sexualized language or imagery and unwelcome sexual attention or
|
|
26
|
-
advances
|
|
27
|
-
* Trolling, insulting/derogatory comments, and personal or political attacks
|
|
28
|
-
* Public or private harassment
|
|
29
|
-
* Publishing others' private information, such as a physical or electronic
|
|
30
|
-
address, without explicit permission
|
|
31
|
-
* Other conduct which could reasonably be considered inappropriate in a
|
|
32
|
-
professional setting
|
|
33
|
-
|
|
34
|
-
## Our Responsibilities
|
|
35
|
-
|
|
36
|
-
Project maintainers are responsible for clarifying the standards of acceptable
|
|
37
|
-
behavior and are expected to take appropriate and fair corrective action in
|
|
38
|
-
response to any instances of unacceptable behavior.
|
|
39
|
-
|
|
40
|
-
Project maintainers have the right and responsibility to remove, edit, or
|
|
41
|
-
reject comments, commits, code, wiki edits, issues, and other contributions
|
|
42
|
-
that are not aligned to this Code of Conduct, or to ban temporarily or
|
|
43
|
-
permanently any contributor for other behaviors that they deem inappropriate,
|
|
44
|
-
threatening, offensive, or harmful.
|
|
45
|
-
|
|
46
|
-
## Scope
|
|
47
|
-
|
|
48
|
-
This Code of Conduct applies both within project spaces and in public spaces
|
|
49
|
-
when an individual is representing the project or its community. Examples of
|
|
50
|
-
representing a project or community include using an official project e-mail
|
|
51
|
-
address, posting via an official social media account, or acting as an appointed
|
|
52
|
-
representative at an online or offline event. Representation of a project may be
|
|
53
|
-
further defined and clarified by project maintainers.
|
|
54
|
-
|
|
55
|
-
## Enforcement
|
|
56
|
-
|
|
57
|
-
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
58
|
-
reported by contacting the project team at molnargerg@gmail.com. All
|
|
59
|
-
complaints will be reviewed and investigated and will result in a response that
|
|
60
|
-
is deemed necessary and appropriate to the circumstances. The project team is
|
|
61
|
-
obligated to maintain confidentiality with regard to the reporter of an incident.
|
|
62
|
-
Further details of specific enforcement policies may be posted separately.
|
|
63
|
-
|
|
64
|
-
Project maintainers who do not follow or enforce the Code of Conduct in good
|
|
65
|
-
faith may face temporary or permanent repercussions as determined by other
|
|
66
|
-
members of the project's leadership.
|
|
67
|
-
|
|
68
|
-
## Attribution
|
|
69
|
-
|
|
70
|
-
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
|
71
|
-
available at [https://contributor-covenant.org/version/1/4][version]
|
|
72
|
-
|
|
73
|
-
[homepage]: https://contributor-covenant.org
|
|
74
|
-
[version]: https://contributor-covenant.org/version/1/4/
|
|
3
|
+
Same as Ruby: [https://www.ruby-lang.org/en/conduct/](https://www.ruby-lang.org/en/conduct/)
|
data/lib/spektr/app.rb
CHANGED
|
@@ -3,10 +3,6 @@ module Spektr
|
|
|
3
3
|
attr_accessor :root, :checks, :initializers, :controllers, :models, :views, :lib_files, :routes, :warnings, :rails_version,
|
|
4
4
|
:production_config, :gem_specs, :ruby_version
|
|
5
5
|
|
|
6
|
-
def self.parser
|
|
7
|
-
@@parser ||= Parser::CurrentRuby
|
|
8
|
-
end
|
|
9
|
-
|
|
10
6
|
def initialize(checks:, ignore: [], root: './')
|
|
11
7
|
@root = root
|
|
12
8
|
@checks = checks
|
|
@@ -21,43 +17,6 @@ module Spektr
|
|
|
21
17
|
@ruby_version = '2.7.1'
|
|
22
18
|
version_file = File.join(root, '.ruby-version')
|
|
23
19
|
@ruby_version = File.read(version_file).lines.first if File.exist?(version_file)
|
|
24
|
-
case @ruby_version
|
|
25
|
-
when /^2\.0\./
|
|
26
|
-
require 'parser/ruby20'
|
|
27
|
-
@@parser = Parser::Ruby20
|
|
28
|
-
when /^2\.1\./
|
|
29
|
-
require 'parser/ruby21'
|
|
30
|
-
@@parser = Parser::Ruby21
|
|
31
|
-
when /^2\.2\./
|
|
32
|
-
require 'parser/ruby22'
|
|
33
|
-
@@parser = Parser::Ruby22
|
|
34
|
-
when /^2\.3/
|
|
35
|
-
require 'parser/ruby23'
|
|
36
|
-
@@parser = Parser::Ruby23
|
|
37
|
-
when /^2\.4\./
|
|
38
|
-
require 'parser/ruby24'
|
|
39
|
-
@@parser = Parser::Ruby24
|
|
40
|
-
when /^2\.5\./
|
|
41
|
-
require 'parser/ruby25'
|
|
42
|
-
@@parser = Parser::Ruby25
|
|
43
|
-
when /^2\.6\./
|
|
44
|
-
require 'parser/ruby26'
|
|
45
|
-
@@parser = Parser::Ruby26
|
|
46
|
-
when /^2\.7\./
|
|
47
|
-
require 'parser/ruby27'
|
|
48
|
-
@@parser = Parser::Ruby27
|
|
49
|
-
when /^3\.0\./
|
|
50
|
-
require 'parser/ruby30'
|
|
51
|
-
@@parser = Parser::Ruby30
|
|
52
|
-
when /^3\.1\./
|
|
53
|
-
require 'parser/ruby31'
|
|
54
|
-
@@parser = Parser::Ruby31
|
|
55
|
-
when /^3\.2\./
|
|
56
|
-
require 'parser/ruby32'
|
|
57
|
-
@@parser = Parser::Ruby32
|
|
58
|
-
else
|
|
59
|
-
@@parser = Parser::CurrentRuby
|
|
60
|
-
end
|
|
61
20
|
end
|
|
62
21
|
|
|
63
22
|
def load
|
|
@@ -94,12 +53,7 @@ module Spektr
|
|
|
94
53
|
# TODO: load non-app lib too
|
|
95
54
|
@lib_files = find_files('lib').map do |path|
|
|
96
55
|
next if loaded_files.include?(path)
|
|
97
|
-
|
|
98
|
-
Targets::Base.new(path, File.read(path, encoding: 'utf-8'))
|
|
99
|
-
rescue Parser::SyntaxError => e
|
|
100
|
-
::Spektr.logger.debug "Couldn't parse #{path}: #{e.message}"
|
|
101
|
-
nil
|
|
102
|
-
end
|
|
56
|
+
Targets::Base.new(path, File.read(path, encoding: 'utf-8'))
|
|
103
57
|
end.reject(&:nil?)
|
|
104
58
|
self
|
|
105
59
|
end
|
|
@@ -158,7 +112,7 @@ module Spektr
|
|
|
158
112
|
name: warning.check.name,
|
|
159
113
|
description: warning.message,
|
|
160
114
|
path: warning.path,
|
|
161
|
-
location: warning.location&.
|
|
115
|
+
location: warning.location&.start_line,
|
|
162
116
|
line: warning.line,
|
|
163
117
|
check: warning.check.class.name,
|
|
164
118
|
fingerprint: warning.fingerprint
|
data/lib/spektr/checks/base.rb
CHANGED
|
@@ -36,19 +36,41 @@ module Spektr
|
|
|
36
36
|
def dupe?(path, location, message)
|
|
37
37
|
@app.warnings.find do |w|
|
|
38
38
|
w.path == path &&
|
|
39
|
-
(w.location.nil? || w.location&.
|
|
39
|
+
(w.location.nil? || w.location&.start_line == location&.start_line) &&
|
|
40
40
|
w.message == message
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def version_affected; end
|
|
45
45
|
|
|
46
|
-
def user_input?(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return
|
|
51
|
-
|
|
46
|
+
def user_input?(node)
|
|
47
|
+
return false if node.nil?
|
|
48
|
+
case node.type
|
|
49
|
+
when :call_node
|
|
50
|
+
return true if %i[params cookies request].include? node.name
|
|
51
|
+
return true if node.receiver && user_input?(node.receiver)
|
|
52
|
+
if node.arguments
|
|
53
|
+
node.arguments.arguments.each do |argument|
|
|
54
|
+
return true if user_input?(argument)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
when :embedded_statements_node
|
|
58
|
+
node.statements.body.each do |item|
|
|
59
|
+
return true if user_input? item
|
|
60
|
+
end
|
|
61
|
+
when :interpolated_string_node, :interpolated_x_string_node
|
|
62
|
+
node.parts.each do |part|
|
|
63
|
+
return true if user_input?(part)
|
|
64
|
+
end
|
|
65
|
+
when :keyword_hash_node, :hash_node
|
|
66
|
+
node.elements.each do |element|
|
|
67
|
+
return true if user_input?(element.key)
|
|
68
|
+
return true if user_input?(element.value)
|
|
69
|
+
end
|
|
70
|
+
# TODO: make this better. ivars can be overridden in the view as well and
|
|
71
|
+
# can be set in non controller targets too
|
|
72
|
+
when :instance_variable_read_node
|
|
73
|
+
return false unless @target.respond_to?(:view_path)
|
|
52
74
|
actions = []
|
|
53
75
|
@app.controllers.each do |controller|
|
|
54
76
|
actions = actions.concat controller.actions.select { |action|
|
|
@@ -56,55 +78,33 @@ module Spektr
|
|
|
56
78
|
}
|
|
57
79
|
end
|
|
58
80
|
actions.each do |action|
|
|
81
|
+
next unless action.body
|
|
59
82
|
action.body.each do |exp|
|
|
60
|
-
return
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
false
|
|
64
|
-
when :send
|
|
65
|
-
if ast.children.first&.type == :send
|
|
66
|
-
child = ast.children.first
|
|
67
|
-
return user_input?(child.type, child.children.last, child)
|
|
68
|
-
end
|
|
69
|
-
return true if %i[params cookies request].include? name
|
|
70
|
-
when :xstr, :begin
|
|
71
|
-
ast.children.each do |child|
|
|
72
|
-
next unless child.is_a?(Parser::AST::Node)
|
|
73
|
-
return true if user_input?(child.type, child.children.last, child)
|
|
74
|
-
end
|
|
75
|
-
when :dstr
|
|
76
|
-
object&.children&.each do |child|
|
|
77
|
-
if child.is_a?(Parser::AST::Node)
|
|
78
|
-
name = nil
|
|
79
|
-
ast = child
|
|
80
|
-
else
|
|
81
|
-
name = child.name
|
|
82
|
-
ast = child.ast
|
|
83
|
+
return true if exp.name == node.name && user_input?(exp)
|
|
83
84
|
end
|
|
84
|
-
return true if user_input?(child.type, name, ast)
|
|
85
85
|
end
|
|
86
|
-
when :
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
when :local_variable_read_node
|
|
87
|
+
return user_input?(@target.lvars.find{|n| n.name == node.name })
|
|
88
|
+
when :instance_variable_write_node, :local_variable_write_node
|
|
89
|
+
return user_input? node.value
|
|
90
|
+
when :parentheses_node
|
|
91
|
+
node.body.body.each do |item|
|
|
92
|
+
return user_input? item
|
|
90
93
|
end
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
next unless child.is_a?(Parser::AST::Node)
|
|
94
|
-
return true if user_input?(child.type, child.children.last, child)
|
|
95
|
-
end
|
|
96
|
-
when :sym, :str, :const, :int, :cbase, :true, :self, :args, :nil, :yield
|
|
94
|
+
|
|
95
|
+
when :string_node, :symbol_node, :constant_read_node, :integer_node, :true_node, :constant_path_node
|
|
97
96
|
# do nothing
|
|
98
97
|
else
|
|
99
|
-
raise "Unknown argument type #{type} #{
|
|
98
|
+
raise "Unknown argument type #{node.type.inspect} #{node.inspect}"
|
|
100
99
|
end
|
|
100
|
+
false
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
# TODO: this doesn't work properly
|
|
104
|
-
def model_attribute?(
|
|
104
|
+
def model_attribute?(node)
|
|
105
105
|
model_names = @app.models.collect(&:name)
|
|
106
|
-
case
|
|
107
|
-
when :
|
|
106
|
+
case node.type
|
|
107
|
+
when :local_variable_read_node, :instance_variable_read_node
|
|
108
108
|
# TODO: handle helpers here too
|
|
109
109
|
if ["Spektr::Targets::Controller", "Spektr::Targets::View"].include?(@target.class.name)
|
|
110
110
|
actions = []
|
|
@@ -115,27 +115,32 @@ module Spektr
|
|
|
115
115
|
end
|
|
116
116
|
actions.each do |action|
|
|
117
117
|
action.body.each do |exp|
|
|
118
|
-
|
|
118
|
+
next unless node.respond_to?(:name)
|
|
119
|
+
return model_attribute?(exp.value) if exp.is_a?(Prism::InstanceVariableWriteNode) && exp.name == node.name
|
|
119
120
|
end
|
|
120
121
|
end
|
|
121
122
|
end
|
|
122
|
-
when :
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
return
|
|
123
|
+
when :call_node
|
|
124
|
+
return model_attribute?(node.receiver) if node.receiver
|
|
125
|
+
if node.arguments
|
|
126
|
+
node.arguments.arguments.each do |argument|
|
|
127
|
+
return true if model_attribute?(argument)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
when :parentheses_node
|
|
131
|
+
node.body.body.each do |item|
|
|
132
|
+
return model_attribute? item
|
|
133
|
+
end
|
|
134
|
+
when :constant_read_node
|
|
135
|
+
return true if model_names.include? node.name.to_s
|
|
136
|
+
when :interpolated_string_node
|
|
137
|
+
node.parts.each do |item|
|
|
138
|
+
return model_attribute? item
|
|
132
139
|
end
|
|
133
|
-
when :
|
|
134
|
-
# TODO: implement this
|
|
135
|
-
when :sym, :str, :nil, :yield
|
|
140
|
+
when :string_node, :symbol_node, :integer_node, :constant_path_node
|
|
136
141
|
# do nothing
|
|
137
142
|
else
|
|
138
|
-
raise "Unknown argument type #{
|
|
143
|
+
raise "Unknown argument type #{node.type}"
|
|
139
144
|
end
|
|
140
145
|
end
|
|
141
146
|
|
|
@@ -147,5 +152,25 @@ module Spektr
|
|
|
147
152
|
version = Gem::Version.new(version) unless version.is_a? Gem::Version
|
|
148
153
|
version >= Gem::Version.new(a) && version <= Gem::Version.new(b)
|
|
149
154
|
end
|
|
155
|
+
|
|
156
|
+
def receivers_for(node)
|
|
157
|
+
receivers = []
|
|
158
|
+
receiver = node.receiver
|
|
159
|
+
while receiver
|
|
160
|
+
receivers << receiver.name
|
|
161
|
+
receiver = receiver.respond_to?(:receiver) ? receiver.receiver : false
|
|
162
|
+
end
|
|
163
|
+
receivers
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def full_receiver(node)
|
|
167
|
+
parents = []
|
|
168
|
+
parent = node.receiver.parent if node.receiver.respond_to?(:parent)
|
|
169
|
+
while parent
|
|
170
|
+
parents << parent.name
|
|
171
|
+
parent = parent.respond_to?(:parent) ? parent.parent : false
|
|
172
|
+
end
|
|
173
|
+
parents.reverse.concat(receivers_for(node).reverse).join(".")
|
|
174
|
+
end
|
|
150
175
|
end
|
|
151
176
|
end
|
|
@@ -17,7 +17,8 @@ module Spektr
|
|
|
17
17
|
def check_filter
|
|
18
18
|
calls = @target.find_calls(:http_basic_authenticate_with)
|
|
19
19
|
calls.each do |call|
|
|
20
|
-
|
|
20
|
+
password = call.arguments.arguments.first.elements.find{|e| e.key.unescaped == "password" }
|
|
21
|
+
if password && password.value.type == :string_node
|
|
21
22
|
warn! @target, self, call.location, "Basic authentication password stored in source code"
|
|
22
23
|
end
|
|
23
24
|
end
|
|
@@ -11,15 +11,15 @@ module Spektr
|
|
|
11
11
|
def run
|
|
12
12
|
return unless super
|
|
13
13
|
# backticks
|
|
14
|
-
@target.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
@target.interpolated_xstrings.each do |call|
|
|
15
|
+
call.parts.each do |part|
|
|
16
|
+
if user_input?(part)
|
|
17
|
+
warn! @target, self, call.location, "Command injection"
|
|
18
|
+
end
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
targets = [
|
|
22
|
+
targets = [:IO, :Open3, :Kernel, :Spawn, :Process, false]
|
|
23
23
|
methods = [:capture2, :capture2e, :capture3, :exec, :pipeline, :pipeline_r,
|
|
24
24
|
:pipeline_rw, :pipeline_start, :pipeline_w, :popen, :popen2, :popen2e,
|
|
25
25
|
:popen3, :spawn, :syscall, :system, :open]
|
|
@@ -32,13 +32,18 @@ module Spektr
|
|
|
32
32
|
|
|
33
33
|
def check_calls(calls)
|
|
34
34
|
# TODO: might need to exclude tempfile and ActiveStorage::Filename
|
|
35
|
+
return if calls.empty?
|
|
35
36
|
calls.each do |call|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
if call.arguments.is_a?(Prism::ArgumentsNode)
|
|
38
|
+
argument = call.arguments.arguments.first
|
|
39
|
+
else
|
|
40
|
+
argument = call.arguments.first
|
|
41
|
+
end
|
|
42
|
+
next unless argument
|
|
43
|
+
if user_input?(argument) || model_attribute?(argument)
|
|
39
44
|
warn! @target, self, call.location, "Command injection in #{call.name}"
|
|
40
45
|
# TODO: interpolation, but might be safe, we should make this better
|
|
41
|
-
elsif
|
|
46
|
+
elsif argument.type == :embedded_statements_node
|
|
42
47
|
warn! @target, self, call.location, "Command injection in #{call.name}", :low
|
|
43
48
|
end
|
|
44
49
|
end
|
|
@@ -29,19 +29,27 @@ module Spektr
|
|
|
29
29
|
cve_2016_6316_check(calls)
|
|
30
30
|
|
|
31
31
|
calls.each do |call|
|
|
32
|
-
call.arguments.
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
call.arguments.is_a?(Prism::ArgumentsNode) ? arguments = call.arguments.arguments : call.arguments
|
|
33
|
+
arguments.each do |argument|
|
|
34
|
+
if user_input?(argument) && @app.rails_version < Gem::Version.new("3.0")
|
|
35
|
+
warn! @target, self, call.location, "Unescaped parameter in content_tag in Rails < 3.0"
|
|
35
36
|
end
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
warn! @target, self, call.location, "Unescaped attribute name in content_tag"
|
|
37
|
+
if argument.is_a?(Prism::KeywordHashNode)
|
|
38
|
+
argument.elements.each do |element|
|
|
39
|
+
if user_input?(element.key)
|
|
40
|
+
warn! @target, self, call.location, "Unescaped parameter in content_tag at #{element.key.name}"
|
|
41
|
+
end
|
|
42
42
|
end
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
|
+
|
|
46
|
+
# if call.options.any?
|
|
47
|
+
# call.options.each_value do |option|
|
|
48
|
+
# if user_input?(option.key.type, option.key.children.last)
|
|
49
|
+
# warn! @target, self, call.location, "Unescaped attribute name in content_tag"
|
|
50
|
+
# end
|
|
51
|
+
# end
|
|
52
|
+
# end
|
|
45
53
|
end
|
|
46
54
|
end
|
|
47
55
|
|
|
@@ -12,7 +12,7 @@ module Spektr
|
|
|
12
12
|
def run
|
|
13
13
|
return unless super
|
|
14
14
|
calls = @target.find_calls(:cookies_serializer=)
|
|
15
|
-
if calls.any?{ |call| call
|
|
15
|
+
if calls.any?{ |call| full_receiver(call) == "Rails.application.config.action_dispatch" && call.arguments.arguments.first.unescaped == "marshal" }
|
|
16
16
|
warn! @target, self, calls.first.location, "Marshal cookie serialization strategy can lead to remote code execution"
|
|
17
17
|
end
|
|
18
18
|
end
|
|
@@ -13,9 +13,9 @@ module Spektr
|
|
|
13
13
|
if app_version_between?("4.0.0", "4.0.8") || app_version_between?("4.1.0", "4.1.5")
|
|
14
14
|
calls = @target.find_calls(:create_with)
|
|
15
15
|
calls.each do |call|
|
|
16
|
-
call.arguments.each do |argument|
|
|
17
|
-
if user_input?(argument
|
|
18
|
-
next if argument.
|
|
16
|
+
call.arguments.arguments.each do |argument|
|
|
17
|
+
if user_input?(argument)
|
|
18
|
+
next if argument.name == :permit
|
|
19
19
|
warn! @target, self, call.location, "create_with is vulnerable to strong params bypass"
|
|
20
20
|
end
|
|
21
21
|
end
|
|
@@ -12,27 +12,13 @@ module Spektr
|
|
|
12
12
|
return unless super
|
|
13
13
|
return if @target.concern?
|
|
14
14
|
|
|
15
|
-
enabled = false
|
|
16
15
|
target = @target
|
|
17
|
-
|
|
18
|
-
parent_controller = target.find_parent(@app.controllers)
|
|
19
|
-
enabled = parent_controller && parent_controller.find_calls(:protect_from_forgery).any?
|
|
20
|
-
break if enabled || parent_controller.nil?
|
|
16
|
+
return if @target.find_calls(:skip_forgery_protection).none?
|
|
21
17
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return if enabled && @target.find_calls(:skip_forgery_protection).none?
|
|
18
|
+
skip = @target.find_calls(:skip_forgery_protection).last
|
|
19
|
+
return if skip && skip.arguments && skip.arguments.arguments.first.elements.map(&:key).map(&:unescaped).intersection(%w[only except]).any?
|
|
25
20
|
|
|
26
|
-
|
|
27
|
-
skip = @target.find_calls(:skip_forgery_protection).last
|
|
28
|
-
return if enabled && skip && skip.options.keys.intersection(%i[only except]).any?
|
|
29
|
-
|
|
30
|
-
warn! @target, self, nil, 'protect_from_forgery should be enabled'
|
|
31
|
-
end
|
|
32
|
-
if @target.find_calls(:skip_forgery_protection).any?
|
|
33
|
-
return @target.find_calls(:skip_forgery_protection).last.options.keys.intersection(%i[only except]).any?
|
|
34
|
-
warn! @target, self, nil, 'protect_from_forgery should be enabled'
|
|
35
|
-
end
|
|
21
|
+
warn! @target, self, nil, 'protect_from_forgery should be enabled'
|
|
36
22
|
end
|
|
37
23
|
end
|
|
38
24
|
end
|
|
@@ -22,11 +22,12 @@ module Spektr
|
|
|
22
22
|
memo
|
|
23
23
|
end
|
|
24
24
|
calls.each do |call|
|
|
25
|
-
|
|
25
|
+
argument_value = call.arguments.arguments.first.unescaped
|
|
26
|
+
if argument_value == ":controller(/:action(/:id(.:format)))" or (argument_value.include?(":controller") && (argument_value.include?(":action") or argument_value.include?("*action")) )
|
|
26
27
|
warn! @target, self, call.location, "All public methods in controllers are available as actions"
|
|
27
28
|
end
|
|
28
29
|
|
|
29
|
-
if
|
|
30
|
+
if argument_value.include?(":action") or argument_value.include?("*action")
|
|
30
31
|
warn! @target, self, call.location, "All public methods in controllers are available as actions"
|
|
31
32
|
end
|
|
32
33
|
end
|
|
@@ -18,41 +18,37 @@ module Spektr
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def check_csv
|
|
21
|
-
check_method(:load,
|
|
21
|
+
check_method(:load, :CSV)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
# TODO: handle safe yaml
|
|
25
25
|
def check_yaml
|
|
26
26
|
[:load_documents, :load_stream, :parse_documents, :parse_stream].each do |method|
|
|
27
|
-
check_method(method,
|
|
27
|
+
check_method(method, :YAML)
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def check_marshal
|
|
32
32
|
[:load, :restore].each do |method|
|
|
33
|
-
check_method(method,
|
|
33
|
+
check_method(method, :Marshal)
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
def check_oj
|
|
38
|
-
check_method(:object_load,
|
|
38
|
+
check_method(:object_load, :Oj)
|
|
39
39
|
safe_default = false
|
|
40
|
-
safe_default = true if @target.find_calls(:mimic_JSON,
|
|
41
|
-
call = @target.find_calls(:default_options=,
|
|
42
|
-
safe_default = true if call && call.
|
|
40
|
+
safe_default = true if @target.find_calls(:mimic_JSON, :Oj).any?
|
|
41
|
+
call = @target.find_calls(:default_options=, :Oj).last
|
|
42
|
+
safe_default = true if call && call.arguments.arguments.first.elements.find{|e| e.key.unescaped == "mode" }.value.unescaped != "object"
|
|
43
43
|
unless safe_default
|
|
44
|
-
check_method(:load,
|
|
44
|
+
check_method(:load, :Oj)
|
|
45
45
|
end
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def check_method(method, receiver)
|
|
49
49
|
calls = @target.find_calls(method, receiver)
|
|
50
50
|
calls.each do |call|
|
|
51
|
-
|
|
52
|
-
if argument.ast.type == :send && argument.ast.children.last.children.first.is_a?(Parser::AST::Node)
|
|
53
|
-
argument = Exp::Argument.new(argument.ast.children.last.children.first)
|
|
54
|
-
end
|
|
55
|
-
if user_input?(argument.type, argument.name, argument.ast)
|
|
51
|
+
if user_input?(call.arguments.arguments.first)
|
|
56
52
|
warn! @target, self, call.location, "#{receiver}.#{method} is called with user supplied value"
|
|
57
53
|
end
|
|
58
54
|
end
|
|
@@ -16,12 +16,12 @@ module Spektr
|
|
|
16
16
|
def run
|
|
17
17
|
return unless super
|
|
18
18
|
call = @target.find_calls(:consider_all_requests_local=).last
|
|
19
|
-
if call && call.arguments.first.type == :
|
|
19
|
+
if call && call.arguments.arguments.first.type == :true_node
|
|
20
20
|
warn! @target, self, call.location, "Detailed exceptions are enabled in production"
|
|
21
21
|
end
|
|
22
22
|
# TODO: make this better, by verifying that the method body is not empty, etc
|
|
23
|
-
if
|
|
24
|
-
warn! @target, self,
|
|
23
|
+
if call = @target.find_method(:show_detailed_exceptions?)
|
|
24
|
+
warn! @target, self, call.location, "Detailed exceptions may be enabled in #{@target.name}"
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
end
|
|
@@ -13,8 +13,8 @@ module Spektr
|
|
|
13
13
|
return unless super
|
|
14
14
|
if app_version_between?("2.0.0", "4.1.99") && @app.has_gem?("mysql")
|
|
15
15
|
@target.find_calls(/^find_by_/).each do |call|
|
|
16
|
-
call.arguments.each do |argument|
|
|
17
|
-
if user_input?(argument
|
|
16
|
+
call.arguments.arguments.each do |argument|
|
|
17
|
+
if user_input?(argument)
|
|
18
18
|
warn! @target, self, call.location, "MySQL integer conversion may cause 0 to match any string"
|
|
19
19
|
end
|
|
20
20
|
end
|
|
@@ -12,8 +12,8 @@ module Spektr
|
|
|
12
12
|
return unless super
|
|
13
13
|
[:eval, :instance_eval, :class_eval, :module_eval].each do |name|
|
|
14
14
|
@target.find_calls(name).each do |call|
|
|
15
|
-
call.arguments.each do |argument|
|
|
16
|
-
if user_input?(argument
|
|
15
|
+
call.arguments.arguments.each do |argument|
|
|
16
|
+
if user_input?(argument)
|
|
17
17
|
warn! @target, self, call.location, "User input in eval"
|
|
18
18
|
end
|
|
19
19
|
end
|