sqreen-alt 1.10.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.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/CODE_OF_CONDUCT.md +22 -0
  3. data/README.md +77 -0
  4. data/Rakefile +20 -0
  5. data/lib/sqreen-alt.rb +1 -0
  6. data/lib/sqreen.rb +68 -0
  7. data/lib/sqreen/attack_detected.html +2 -0
  8. data/lib/sqreen/binding_accessor.rb +288 -0
  9. data/lib/sqreen/ca.crt +72 -0
  10. data/lib/sqreen/call_countable.rb +67 -0
  11. data/lib/sqreen/callback_tree.rb +78 -0
  12. data/lib/sqreen/callbacks.rb +100 -0
  13. data/lib/sqreen/capped_queue.rb +23 -0
  14. data/lib/sqreen/condition_evaluator.rb +235 -0
  15. data/lib/sqreen/conditionable.rb +50 -0
  16. data/lib/sqreen/configuration.rb +168 -0
  17. data/lib/sqreen/context.rb +26 -0
  18. data/lib/sqreen/deliveries/batch.rb +84 -0
  19. data/lib/sqreen/deliveries/simple.rb +39 -0
  20. data/lib/sqreen/event.rb +16 -0
  21. data/lib/sqreen/events/attack.rb +61 -0
  22. data/lib/sqreen/events/remote_exception.rb +54 -0
  23. data/lib/sqreen/events/request_record.rb +62 -0
  24. data/lib/sqreen/exception.rb +34 -0
  25. data/lib/sqreen/frameworks.rb +40 -0
  26. data/lib/sqreen/frameworks/generic.rb +446 -0
  27. data/lib/sqreen/frameworks/rails.rb +148 -0
  28. data/lib/sqreen/frameworks/rails3.rb +36 -0
  29. data/lib/sqreen/frameworks/request_recorder.rb +69 -0
  30. data/lib/sqreen/frameworks/sinatra.rb +57 -0
  31. data/lib/sqreen/frameworks/sqreen_test.rb +26 -0
  32. data/lib/sqreen/instrumentation.rb +542 -0
  33. data/lib/sqreen/log.rb +119 -0
  34. data/lib/sqreen/metrics.rb +6 -0
  35. data/lib/sqreen/metrics/average.rb +39 -0
  36. data/lib/sqreen/metrics/base.rb +45 -0
  37. data/lib/sqreen/metrics/collect.rb +22 -0
  38. data/lib/sqreen/metrics/sum.rb +20 -0
  39. data/lib/sqreen/metrics_store.rb +96 -0
  40. data/lib/sqreen/middleware.rb +34 -0
  41. data/lib/sqreen/payload_creator.rb +137 -0
  42. data/lib/sqreen/performance_notifications.rb +86 -0
  43. data/lib/sqreen/performance_notifications/log.rb +36 -0
  44. data/lib/sqreen/performance_notifications/metrics.rb +36 -0
  45. data/lib/sqreen/performance_notifications/newrelic.rb +36 -0
  46. data/lib/sqreen/remote_command.rb +93 -0
  47. data/lib/sqreen/rule_attributes.rb +26 -0
  48. data/lib/sqreen/rule_callback.rb +108 -0
  49. data/lib/sqreen/rules.rb +126 -0
  50. data/lib/sqreen/rules_callbacks.rb +29 -0
  51. data/lib/sqreen/rules_callbacks/binding_accessor_matcher.rb +77 -0
  52. data/lib/sqreen/rules_callbacks/binding_accessor_metrics.rb +79 -0
  53. data/lib/sqreen/rules_callbacks/blacklist_ips.rb +44 -0
  54. data/lib/sqreen/rules_callbacks/count_http_codes.rb +40 -0
  55. data/lib/sqreen/rules_callbacks/crawler_user_agent_matches.rb +24 -0
  56. data/lib/sqreen/rules_callbacks/crawler_user_agent_matches_metrics.rb +24 -0
  57. data/lib/sqreen/rules_callbacks/custom_error.rb +64 -0
  58. data/lib/sqreen/rules_callbacks/execjs.rb +241 -0
  59. data/lib/sqreen/rules_callbacks/headers_insert.rb +22 -0
  60. data/lib/sqreen/rules_callbacks/inspect_rule.rb +25 -0
  61. data/lib/sqreen/rules_callbacks/matcher_rule.rb +138 -0
  62. data/lib/sqreen/rules_callbacks/rails_parameters.rb +14 -0
  63. data/lib/sqreen/rules_callbacks/record_request_context.rb +39 -0
  64. data/lib/sqreen/rules_callbacks/reflected_xss.rb +254 -0
  65. data/lib/sqreen/rules_callbacks/regexp_rule.rb +36 -0
  66. data/lib/sqreen/rules_callbacks/shell_env.rb +32 -0
  67. data/lib/sqreen/rules_callbacks/url_matches.rb +25 -0
  68. data/lib/sqreen/rules_callbacks/user_agent_matches.rb +22 -0
  69. data/lib/sqreen/rules_signature.rb +151 -0
  70. data/lib/sqreen/runner.rb +365 -0
  71. data/lib/sqreen/runtime_infos.rb +138 -0
  72. data/lib/sqreen/safe_json.rb +60 -0
  73. data/lib/sqreen/sdk.rb +22 -0
  74. data/lib/sqreen/serializer.rb +46 -0
  75. data/lib/sqreen/session.rb +317 -0
  76. data/lib/sqreen/shared_storage.rb +31 -0
  77. data/lib/sqreen/stats.rb +18 -0
  78. data/lib/sqreen/version.rb +5 -0
  79. metadata +148 -0
@@ -0,0 +1,22 @@
1
+ # Copyright (c) 2015 Sqreen. All Rights Reserved.
2
+ # Please refer to our terms for more information: https://www.sqreen.io/terms.html
3
+
4
+ require 'sqreen/rule_callback'
5
+
6
+ module Sqreen
7
+ module Rules
8
+ # Display sqreen presence
9
+ class HeadersInsertCB < RuleCB
10
+ def post(rv, _inst, *_args, &_block)
11
+ return unless rv && rv.respond_to?(:[]) && rv[1].is_a?(Hash)
12
+ return nil unless @data
13
+ headers = @data['values'] || []
14
+ return if headers.empty?
15
+ headers.each do |name, value|
16
+ rv[1][name] = value
17
+ end
18
+ advise_action(nil)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ # Copyright (c) 2015 Sqreen. All Rights Reserved.
2
+ # Please refer to our terms for more information: https://www.sqreen.io/terms.html
3
+
4
+ require 'sqreen/rule_callback'
5
+
6
+ module Sqreen
7
+ module Rules
8
+ class InspectRuleCB < RuleCB
9
+ def pre(_inst, *args, &_block)
10
+ Sqreen.log.debug { "<< #{@klass} #{@method} #{Thread.current}" }
11
+ Sqreen.log.debug { args.map(&:inspect).join(' ') }
12
+ end
13
+
14
+ def post(rv, _inst, *_args, &_block)
15
+ Sqreen.log.debug { ">> #{rv.inspect} #{@klass} #{@method} #{Thread.current}" }
16
+ byebug if defined? byebug && @data.is_a?(Hash) && @data[:break] == 1
17
+ end
18
+
19
+ def failing(rv, _inst, *_args, &_block)
20
+ Sqreen.log.debug { "># #{rv.inspect} #{@klass} #{@method} #{Thread.current}" }
21
+ byebug if defined? byebug && @data.is_a?(Hash) && @data[:break] == 1
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,138 @@
1
+ # Copyright (c) 2015 Sqreen. All Rights Reserved.
2
+ # Please refer to our terms for more information: https://www.sqreen.io/terms.html
3
+
4
+ require 'sqreen/rule_callback'
5
+
6
+ module Sqreen
7
+ module Rules
8
+ # matcher behavior
9
+ module Matcher
10
+ attr_reader :min_size
11
+ def self.prepare_re_pattern(value, options, case_sensitive)
12
+ res = 0
13
+ res |= Regexp::MULTILINE if options.include?('multiline')
14
+ res |= Regexp::IGNORECASE unless case_sensitive
15
+ r = Regexp.compile(value, res)
16
+ r.match("")
17
+ r
18
+ end
19
+
20
+ ANYWHERE_OPT = 'anywhere'.freeze
21
+ def prepare(patterns)
22
+ @string = {}
23
+ @regexp_patterns = []
24
+
25
+ if patterns.nil?
26
+ msg = "no key 'values' in data (had #{@data.keys})"
27
+ raise Sqreen::Exception, msg
28
+ end
29
+
30
+ @funs = {
31
+ ANYWHERE_OPT => lambda { |value, str| str.include?(value) },
32
+ 'starts_with'.freeze => lambda { |value, str| str.start_with?(value) },
33
+ 'ends_with'.freeze => lambda { |value, str| str.end_with?(value) },
34
+ 'equals'.freeze => lambda { |value, str| str == value },
35
+ }
36
+
37
+ sizes = []
38
+ patterns.each do |entry|
39
+ next unless entry
40
+ type = entry['type']
41
+ val = entry['value']
42
+ opts = entry['options']
43
+ opt = ANYWHERE_OPT
44
+ opt = opts.first.freeze if opts && opts.first && opts.first != ''
45
+ case_sensitive = entry['case_sensitive'] || false
46
+ case type
47
+ when 'string'
48
+ if case_sensitive
49
+ case_type = :cs
50
+ else
51
+ case_type = :ci
52
+ val.downcase!
53
+ end
54
+
55
+ unless @funs.keys.include?(opt)
56
+ Sqreen.log.debug { "Error: unknown string option '#{opt}' " }
57
+ next
58
+ end
59
+ @string[opt] = { :ci => [], :cs => [] } unless @string.key?(opt)
60
+ @string[opt][case_type] << val
61
+ sizes << entry.fetch('min_length') { val.size }
62
+ when 'regexp'
63
+ pattern = Matcher.prepare_re_pattern(val, opt, case_sensitive)
64
+ next unless pattern
65
+ @regexp_patterns << pattern
66
+ sizes << entry['min_length']
67
+ else
68
+ raise Sqreen::Exception, "No such matcher type #{type}"
69
+ end
70
+ end
71
+
72
+ @min_size = sizes.min unless sizes.any?(&:nil?)
73
+
74
+ return unless [@regexp_patterns, @string].map(&:empty?).all?
75
+ msg = "no key 'regexp' nor 'match' in data (had #{@data.keys})"
76
+ raise Sqreen::Exception, msg
77
+ end
78
+
79
+ def match(str)
80
+ return if str.nil? || str.empty? || !str.is_a?(String)
81
+ str = enforce_encoding(str) unless str.ascii_only?
82
+ istr = str.downcase unless @string.empty?
83
+
84
+ @string.each do |type, cases|
85
+ fun = @funs[type]
86
+ if fun.nil?
87
+ Sqreen.log.debug { "no matching function found for type #{type}" }
88
+ end
89
+ cases.each do |case_type, patterns|
90
+ input_str = if case_type == :ci
91
+ istr
92
+ else
93
+ str
94
+ end
95
+ patterns.each do |pat|
96
+ return pat if fun.call(pat, input_str)
97
+ end
98
+ end
99
+ end
100
+
101
+ if defined?(Encoding)
102
+ @regexp_patterns.each do |p|
103
+ next unless Encoding.compatible?(p, str)
104
+ return p if p.match(str)
105
+ end
106
+ else
107
+ @regexp_patterns.each do |p|
108
+ return p if p.match(str)
109
+ end
110
+ end
111
+ nil
112
+ end
113
+
114
+ private
115
+
116
+ def enforce_encoding(str)
117
+ encoded8bit = str.encoding.name == 'ASCII-8BIT'
118
+ return str if !encoded8bit && str.valid_encoding?
119
+ str.chars.map do |v|
120
+ if !v.valid_encoding? || (encoded8bit && !v.ascii_only?)
121
+ ''
122
+ else
123
+ v
124
+ end
125
+ end.join
126
+ end
127
+ end
128
+
129
+ # A configurable matcher rule
130
+ class MatcherRuleCB < RuleCB
131
+ def initialize(*args)
132
+ super(*args)
133
+ prepare(@data['values'])
134
+ end
135
+ include Matcher
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,14 @@
1
+ # Copyright (c) 2015 Sqreen. All Rights Reserved.
2
+ # Please refer to our terms for more information: https://www.sqreen.io/terms.html
3
+
4
+ require 'sqreen/rule_callback'
5
+
6
+ module Sqreen
7
+ module Rules
8
+ class RailsParametersCB < RuleCB
9
+ def pre(_inst, *_args, &_block)
10
+ advise_action(nil)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,39 @@
1
+ # Copyright (c) 2015 Sqreen. All Rights Reserved.
2
+ # Please refer to our terms for more information: https://www.sqreen.io/terms.html
3
+
4
+ require 'sqreen/rule_callback'
5
+ require 'sqreen/instrumentation'
6
+
7
+ module Sqreen
8
+ module Rules
9
+ # Save request context for handling further down
10
+ class RecordRequestContext < RuleCB
11
+ def whitelisted?
12
+ false
13
+ end
14
+
15
+ def pre(_inst, *args, &_block)
16
+ framework.store_request(args[0])
17
+ wh = framework.whitelisted_match
18
+ if wh
19
+ unless Sqreen.features.key?('whitelisted_metric') &&
20
+ !Sqreen.features['whitelisted_metric']
21
+ record_observation(Instrumentation::WHITELISTED_METRIC, wh, 1)
22
+ end
23
+ Sqreen.log.debug { "Request was whitelisted because of #{wh}" }
24
+ end
25
+ advise_action(nil)
26
+ end
27
+
28
+ def post(_rv, _inst, *_args, &_block)
29
+ framework.clean_request
30
+ advise_action(nil)
31
+ end
32
+
33
+ def failing(_exception, _inst, *_args, &_block)
34
+ framework.clean_request
35
+ advise_action(nil)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,254 @@
1
+ # Copyright (c) 2015 Sqreen. All Rights Reserved.
2
+ # Please refer to our terms for more information: https://www.sqreen.io/terms.html
3
+
4
+ require 'cgi'
5
+
6
+ require 'sqreen/rule_callback'
7
+ require 'sqreen/rules_callbacks/regexp_rule'
8
+
9
+ # Sqreen module
10
+ module Sqreen
11
+ # Sqreen rules
12
+ module Rules
13
+ # XSSCB abstract common behaviour of tpls
14
+ class XSSCB < RegexpRuleCB
15
+ # The remaining code is only to find out if user entry was an attack,
16
+ # and record it. Since we don't rely on it to respond to user, it would
17
+ # be better to do it in background.
18
+ def report_dangerous_xss?(value)
19
+ found = match_regexp(value)
20
+
21
+ return false unless found
22
+ infos = {
23
+ :found => found,
24
+ :payload => value,
25
+ }
26
+ record_event(infos)
27
+ true
28
+ end
29
+ end
30
+ class ReflectedUnsafeXSSCB < XSSCB
31
+ def pre(_inst, *args, &_block)
32
+ value = args[0]
33
+
34
+ return unless value.is_a?(String)
35
+
36
+ # Sqreen::log.debug value
37
+
38
+ return unless framework.params_include?(value)
39
+
40
+ Sqreen.log.debug { format('Found unescaped user param: %s', value) }
41
+
42
+ saved_value = value.dup
43
+ return unless report_dangerous_xss?(saved_value)
44
+
45
+ # potential XSS! let's escape
46
+ if block
47
+ args[0].replace(CGI.escape_html(value))
48
+ end
49
+
50
+ advise_action(nil)
51
+ end
52
+ end
53
+ # look for reflected XSS with erb template engine
54
+ class ReflectedXSSCB < XSSCB
55
+ def pre(_inst, *args, &_block)
56
+ value = args[0]
57
+
58
+ return unless value.is_a?(String)
59
+
60
+ # If the value is not marked as html_safe, it will be escaped later
61
+ return unless value.html_safe?
62
+
63
+ # Sqreen::log.debug value
64
+
65
+ return unless framework.params_include?(value)
66
+
67
+ Sqreen.log.debug { format('Found unescaped user param: %s', value) }
68
+
69
+ saved_value = value.dup
70
+ return unless report_dangerous_xss?(saved_value)
71
+
72
+ # potential XSS! let's escape
73
+ if block
74
+ args[0].replace(CGI.escape_html(value))
75
+ end
76
+
77
+ advise_action(nil)
78
+ end
79
+ end
80
+ # look for reflected XSS with haml template engine
81
+ # hook function arguments of
82
+ # Haml::Buffer.format_script(result, preserve_script, in_tag, preserve_tag,
83
+ # escape_html, nuke_inner_whitespace,
84
+ # interpolated, ugly)
85
+ class ReflectedXSSHamlCB < XSSCB
86
+ def post(ret, _inst, *_args, &_block)
87
+ value = ret
88
+ return if value.nil?
89
+
90
+ # Sqreen::log.debug value
91
+
92
+ return unless framework.full_params_include?(value)
93
+
94
+ Sqreen.log.debug { format('Found unescaped user param: %s', value) }
95
+
96
+ return unless value.is_a?(String)
97
+
98
+ return unless report_dangerous_xss?(value)
99
+
100
+ return unless block
101
+ # potential XSS! let's escape
102
+ advise_action(:override, :new_return_value => CGI.escape_html(value))
103
+ end
104
+ end
105
+
106
+ # Hook into haml4 script parser
107
+ class Haml4ParserScriptHookCB < RuleCB
108
+ def pre(_inst, *args, &_block)
109
+ return unless args.size > 1
110
+ return unless Haml::VERSION < '5'
111
+ text = args[0]
112
+ escape_html = args[1]
113
+ if escape_html == false &&
114
+ text.respond_to?(:include?) &&
115
+ !text.include?('html_escape')
116
+ args[0].replace("Sqreen.escape_haml((#{args[0]}))")
117
+ end
118
+ nil
119
+ end
120
+ end
121
+
122
+ # Hook into haml4 tag parser
123
+ class Haml4ParserTagHookCB < RuleCB
124
+ def post(ret, _inst, *_args, &_block)
125
+ return unless Haml::VERSION < '5'
126
+ tag = ret
127
+ if tag.value[:escape_html] == false &&
128
+ tag.value[:value].respond_to?(:include?) &&
129
+ !tag.value[:value].include?('html_escape')
130
+ tag.value[:value] = "Sqreen.escape_haml((#{tag.value[:value]}))"
131
+ return { :status => :override, :new_return_value => tag }
132
+ end
133
+ nil
134
+ end
135
+ end
136
+
137
+ class Haml4UtilInterpolationHookCB < RuleCB
138
+ def pre(_inst, *args, &_block)
139
+ # Also work in haml5
140
+ str = args[0]
141
+ escape_html = args[1]
142
+ # Original code from HAML tuned up to insert escape_haml call
143
+ res = ''
144
+ rest = Haml::Util.handle_interpolation str.dump do |scan|
145
+ escapes = (scan[2].size - 1) / 2
146
+ res << scan.matched[0...-3 - escapes]
147
+ if escapes.odd?
148
+ res << '#{'
149
+ else
150
+ content = eval('"' + Haml::Util.balance(scan, '{', '}', 1)[0][0...-1] + '"')
151
+ content = "Haml::Helpers.html_escape((#{content}))" if escape_html
152
+ res << '#{Sqreen.escape_haml((' + content + '))}' # Use eval to get rid of string escapes
153
+ end
154
+ end
155
+ { :status => :skip, :new_return_value => res + rest }
156
+ end
157
+ end
158
+
159
+ # Hook build attributes
160
+ class Haml4CompilerBuildAttributeCB < XSSCB
161
+ def pre(inst, *args, &_block)
162
+ return unless Haml::VERSION < '5'
163
+ attrs = args[-1]
164
+ new_attrs, found_xss = Haml4CompilerBuildAttributeCB.clean_hash_key(attrs) do |key|
165
+ if !key.nil? && key.is_a?(String) && framework.full_params_include?(key) && report_dangerous_xss?(key)
166
+ Sqreen.log.debug { format('Found unescaped user param: %s', key) }
167
+ [CGI.escape_html(key), true]
168
+ else
169
+ [key, false]
170
+ end
171
+ end
172
+
173
+ return if !found_xss || !block
174
+ # potential XSS! let's escape
175
+ args[-1] = new_attrs
176
+ r = inst.send(method, *args)
177
+ { :status => :skip, :new_return_value => r }
178
+ end
179
+
180
+ def self.clean_hash_key(hash, limit = 10, seen = [], &block)
181
+ seen << hash.object_id
182
+ has_xss = false
183
+ new_h = {}
184
+ return if limit <= 0
185
+ hash.each do |k, v|
186
+ if seen.include?(v.object_id)
187
+ new_h[k] = nil
188
+ next
189
+ end
190
+ seen << v.object_id
191
+ new_key, found_xss = yield k
192
+ has_xss |= found_xss
193
+ if v.is_a?(Hash)
194
+ new_h[new_key], found_xss = Haml4CompilerBuildAttributeCB.clean_hash_key(v, limit - 1, seen, &block)
195
+ has_xss |= found_xss
196
+ else
197
+ new_h[new_key] = v
198
+ end
199
+ end
200
+ [new_h, has_xss]
201
+ end
202
+ end
203
+
204
+ class Haml5EscapableHookCB < RuleCB
205
+ def pre(_inst, *args, &_block)
206
+ args[0] = "Sqreen.escape_haml((#{args[0]}))"
207
+ { :status => :modify_args, :args => args }
208
+ end
209
+ end
210
+
211
+ # Hook into temple template rendering
212
+ class TempleEscapableHookCB < RuleCB
213
+ def post(ret, _inst, *_args, &_block)
214
+ ret[1] = "Sqreen.escape_temple((#{ret[1]}))"
215
+ { :status => :override, :new_return_value => ret }
216
+ end
217
+ end
218
+
219
+ # Hook into temple template rendering
220
+ class SlimSplatBuilderCB < XSSCB
221
+ def pre(inst, *args, &_block)
222
+ value = args[0]
223
+ return if value.nil?
224
+
225
+ return unless framework.full_params_include?(value)
226
+
227
+ Sqreen.log.debug { format('Found unescaped user param: %s', value) }
228
+
229
+ return unless value.is_a?(String)
230
+
231
+ return unless report_dangerous_xss?(value)
232
+
233
+ return unless block
234
+ # potential XSS! let's escape
235
+ if block
236
+ args[0] = CGI.escape_html(value)
237
+ r = inst.send(method, *args)
238
+ return { :status => :skip, :new_return_value => r }
239
+ end
240
+ nil
241
+ end
242
+ end
243
+ end
244
+
245
+ # Escape HAML when instrumented to do it
246
+ def self.escape_haml(x)
247
+ x
248
+ end
249
+
250
+ # Escape Temple when instrumented to do it
251
+ def self.escape_temple(x)
252
+ x
253
+ end
254
+ end