tainted_love 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (203) hide show
  1. checksums.yaml +7 -0
  2. data/.github/probots.yml +2 -0
  3. data/.gitignore +12 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +1188 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +7 -0
  8. data/CODE_OF_CONDUCT.md +73 -0
  9. data/Gemfile +8 -0
  10. data/Gemfile.lock +57 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +85 -0
  13. data/Rakefile +8 -0
  14. data/bin/console +15 -0
  15. data/bin/setup +10 -0
  16. data/bin/test +7 -0
  17. data/dev.yml +28 -0
  18. data/docs/TaintedLove.html +482 -0
  19. data/docs/TaintedLove/Configuration.html +499 -0
  20. data/docs/TaintedLove/Replacer.html +129 -0
  21. data/docs/TaintedLove/Replacer/ActionViewHelpersMod.html +230 -0
  22. data/docs/TaintedLove/Replacer/Base.html +320 -0
  23. data/docs/TaintedLove/Replacer/HelperMod.html +226 -0
  24. data/docs/TaintedLove/Replacer/HelpersMod.html +230 -0
  25. data/docs/TaintedLove/Replacer/MarshalMod.html +178 -0
  26. data/docs/TaintedLove/Replacer/ObjectMod.html +282 -0
  27. data/docs/TaintedLove/Replacer/ReplaceActionController.html +329 -0
  28. data/docs/TaintedLove/Replacer/ReplaceActionView.html +317 -0
  29. data/docs/TaintedLove/Replacer/ReplaceActiveRecord.html +341 -0
  30. data/docs/TaintedLove/Replacer/ReplaceDigest.html +369 -0
  31. data/docs/TaintedLove/Replacer/ReplaceFile.html +245 -0
  32. data/docs/TaintedLove/Replacer/ReplaceKernel.html +211 -0
  33. data/docs/TaintedLove/Replacer/ReplaceMarshal.html +219 -0
  34. data/docs/TaintedLove/Replacer/ReplaceObject.html +231 -0
  35. data/docs/TaintedLove/Replacer/ReplaceRailsUserInput.html +374 -0
  36. data/docs/TaintedLove/Replacer/ReplaceSprokets.html +297 -0
  37. data/docs/TaintedLove/Replacer/SprocketsHelperMod.html +226 -0
  38. data/docs/TaintedLove/Reporter.html +117 -0
  39. data/docs/TaintedLove/Reporter/Base.html +466 -0
  40. data/docs/TaintedLove/Reporter/RackReporter.html +309 -0
  41. data/docs/TaintedLove/Reporter/SinatraReporter.html +402 -0
  42. data/docs/TaintedLove/Reporter/SinatraReporter/App.html +210 -0
  43. data/docs/TaintedLove/Reporter/StdoutReporter.html +305 -0
  44. data/docs/TaintedLove/SinatraReporter.html +387 -0
  45. data/docs/TaintedLove/SinatraReporter/App.html +210 -0
  46. data/docs/TaintedLove/StackTrace.html +650 -0
  47. data/docs/TaintedLove/Utils.html +550 -0
  48. data/docs/TaintedLove/Validator.html +129 -0
  49. data/docs/TaintedLove/Validator/ActionViewObjectSend.html +233 -0
  50. data/docs/TaintedLove/Validator/Base.html +200 -0
  51. data/docs/TaintedLove/Validator/ErbEval.html +229 -0
  52. data/docs/TaintedLove/Validator/RedisStoreSerialization.html +238 -0
  53. data/docs/TaintedLove/Validator/SproketsMarshal.html +233 -0
  54. data/docs/TaintedLove/Warning.html +665 -0
  55. data/docs/_index.html +371 -0
  56. data/docs/class_list.html +51 -0
  57. data/docs/css/common.css +1 -0
  58. data/docs/css/full_list.css +58 -0
  59. data/docs/css/style.css +496 -0
  60. data/docs/file.README.html +134 -0
  61. data/docs/file_list.html +56 -0
  62. data/docs/frames.html +17 -0
  63. data/docs/index.html +134 -0
  64. data/docs/js/app.js +292 -0
  65. data/docs/js/full_list.js +216 -0
  66. data/docs/js/jquery.js +4 -0
  67. data/docs/method_list.html +523 -0
  68. data/docs/top-level-namespace.html +110 -0
  69. data/example/.gitignore +31 -0
  70. data/example/.ruby-version +1 -0
  71. data/example/Gemfile +67 -0
  72. data/example/Gemfile.lock +226 -0
  73. data/example/README.md +24 -0
  74. data/example/Rakefile +8 -0
  75. data/example/app/assets/config/manifest.js +3 -0
  76. data/example/app/assets/images/.keep +0 -0
  77. data/example/app/assets/javascripts/application.js +16 -0
  78. data/example/app/assets/javascripts/cable.js +13 -0
  79. data/example/app/assets/javascripts/channels/.keep +0 -0
  80. data/example/app/assets/javascripts/products.coffee +3 -0
  81. data/example/app/assets/stylesheets/application.css +15 -0
  82. data/example/app/assets/stylesheets/products.scss +3 -0
  83. data/example/app/assets/stylesheets/scaffolds.scss +84 -0
  84. data/example/app/channels/application_cable/channel.rb +6 -0
  85. data/example/app/channels/application_cable/connection.rb +6 -0
  86. data/example/app/controllers/application_controller.rb +4 -0
  87. data/example/app/controllers/concerns/.keep +0 -0
  88. data/example/app/controllers/products_controller.rb +77 -0
  89. data/example/app/controllers/test_cases_controller.rb +20 -0
  90. data/example/app/helpers/application_helper.rb +4 -0
  91. data/example/app/helpers/products_helper.rb +4 -0
  92. data/example/app/helpers/test_cases_helper.rb +4 -0
  93. data/example/app/jobs/application_job.rb +4 -0
  94. data/example/app/mailers/application_mailer.rb +6 -0
  95. data/example/app/models/application_record.rb +5 -0
  96. data/example/app/models/concerns/.keep +0 -0
  97. data/example/app/models/product.rb +4 -0
  98. data/example/app/views/layouts/application.html.erb +15 -0
  99. data/example/app/views/layouts/mailer.html.erb +13 -0
  100. data/example/app/views/layouts/mailer.text.erb +1 -0
  101. data/example/app/views/products/_form.html.erb +32 -0
  102. data/example/app/views/products/_product.json.jbuilder +4 -0
  103. data/example/app/views/products/edit.html.erb +6 -0
  104. data/example/app/views/products/index.html.erb +31 -0
  105. data/example/app/views/products/index.json.jbuilder +3 -0
  106. data/example/app/views/products/new.html.erb +5 -0
  107. data/example/app/views/products/show.html.erb +19 -0
  108. data/example/app/views/products/show.json.jbuilder +3 -0
  109. data/example/app/views/test_cases/xss.html.erb +10 -0
  110. data/example/bin/bundle +5 -0
  111. data/example/bin/rails +11 -0
  112. data/example/bin/rake +11 -0
  113. data/example/bin/setup +38 -0
  114. data/example/bin/spring +18 -0
  115. data/example/bin/update +33 -0
  116. data/example/bin/yarn +11 -0
  117. data/example/config.ru +7 -0
  118. data/example/config/application.rb +21 -0
  119. data/example/config/boot.rb +6 -0
  120. data/example/config/cable.yml +10 -0
  121. data/example/config/credentials.yml.enc +1 -0
  122. data/example/config/database.yml +25 -0
  123. data/example/config/environment.rb +7 -0
  124. data/example/config/environments/development.rb +63 -0
  125. data/example/config/environments/production.rb +96 -0
  126. data/example/config/environments/test.rb +48 -0
  127. data/example/config/initializers/application_controller_renderer.rb +10 -0
  128. data/example/config/initializers/assets.rb +16 -0
  129. data/example/config/initializers/backtrace_silencers.rb +9 -0
  130. data/example/config/initializers/content_security_policy.rb +27 -0
  131. data/example/config/initializers/cookies_serializer.rb +7 -0
  132. data/example/config/initializers/filter_parameter_logging.rb +6 -0
  133. data/example/config/initializers/inflections.rb +18 -0
  134. data/example/config/initializers/mime_types.rb +6 -0
  135. data/example/config/initializers/tainted_love.rb +7 -0
  136. data/example/config/initializers/wrap_parameters.rb +16 -0
  137. data/example/config/locales/en.yml +33 -0
  138. data/example/config/puma.rb +36 -0
  139. data/example/config/routes.rb +10 -0
  140. data/example/config/spring.rb +8 -0
  141. data/example/config/storage.yml +34 -0
  142. data/example/db/migrate/20190311220346_create_products.rb +13 -0
  143. data/example/db/schema.rb +23 -0
  144. data/example/db/seeds.rb +9 -0
  145. data/example/lib/assets/.keep +0 -0
  146. data/example/lib/tasks/.keep +0 -0
  147. data/example/log/.keep +0 -0
  148. data/example/package.json +5 -0
  149. data/example/public/404.html +67 -0
  150. data/example/public/422.html +67 -0
  151. data/example/public/500.html +66 -0
  152. data/example/public/apple-touch-icon-precomposed.png +0 -0
  153. data/example/public/apple-touch-icon.png +0 -0
  154. data/example/public/favicon.ico +0 -0
  155. data/example/public/robots.txt +1 -0
  156. data/example/storage/.keep +0 -0
  157. data/example/test/application_system_test_case.rb +7 -0
  158. data/example/test/controllers/.keep +0 -0
  159. data/example/test/controllers/products_controller_test.rb +66 -0
  160. data/example/test/controllers/test_cases_controller_test.rb +39 -0
  161. data/example/test/fixtures/.keep +0 -0
  162. data/example/test/fixtures/files/.keep +0 -0
  163. data/example/test/fixtures/products.yml +11 -0
  164. data/example/test/helpers/.keep +0 -0
  165. data/example/test/integration/.keep +0 -0
  166. data/example/test/mailers/.keep +0 -0
  167. data/example/test/models/.keep +0 -0
  168. data/example/test/models/product_test.rb +9 -0
  169. data/example/test/replacers/replace_active_record_test.rb +31 -0
  170. data/example/test/replacers/replace_sprokets_test.rb +8 -0
  171. data/example/test/system/.keep +0 -0
  172. data/example/test/system/products_test.rb +49 -0
  173. data/example/test/test_helper.rb +37 -0
  174. data/example/tmp/.keep +0 -0
  175. data/example/vendor/.keep +0 -0
  176. data/lib/tainted_love.rb +57 -0
  177. data/lib/tainted_love/configuration.rb +16 -0
  178. data/lib/tainted_love/replacer/base.rb +25 -0
  179. data/lib/tainted_love/replacer/replace_action_controller.rb +61 -0
  180. data/lib/tainted_love/replacer/replace_action_view.rb +39 -0
  181. data/lib/tainted_love/replacer/replace_active_record.rb +47 -0
  182. data/lib/tainted_love/replacer/replace_digest.rb +39 -0
  183. data/lib/tainted_love/replacer/replace_file.rb +32 -0
  184. data/lib/tainted_love/replacer/replace_kernel.rb +44 -0
  185. data/lib/tainted_love/replacer/replace_marshal.rb +19 -0
  186. data/lib/tainted_love/replacer/replace_object.rb +30 -0
  187. data/lib/tainted_love/replacer/replace_rails_user_input.rb +59 -0
  188. data/lib/tainted_love/replacer/replace_sprokets.rb +25 -0
  189. data/lib/tainted_love/replacer/replace_yaml.rb +28 -0
  190. data/lib/tainted_love/reporter/base.rb +47 -0
  191. data/lib/tainted_love/reporter/file_reporter.rb +28 -0
  192. data/lib/tainted_love/reporter/stdout_reporter.rb +30 -0
  193. data/lib/tainted_love/stack_trace.rb +46 -0
  194. data/lib/tainted_love/utils.rb +80 -0
  195. data/lib/tainted_love/validator/action_view_object_send.rb +15 -0
  196. data/lib/tainted_love/validator/base.rb +16 -0
  197. data/lib/tainted_love/validator/erb_eval.rb +13 -0
  198. data/lib/tainted_love/validator/redis_store_serialization.rb +13 -0
  199. data/lib/tainted_love/validator/sprokets_marshal.rb +15 -0
  200. data/lib/tainted_love/version.rb +5 -0
  201. data/lib/tainted_love/warning.rb +30 -0
  202. data/tainted_love.gemspec +31 -0
  203. metadata +315 -0
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ Dir[File.dirname(__FILE__) + '/tainted_love/**/*.rb'].each { |f| require f }
4
+
5
+ module TaintedLove
6
+ class << self
7
+ include TaintedLove::Utils
8
+
9
+ attr_reader :configuration
10
+
11
+ # Enables TaintedLove. Use a block to configure the TaintedLove::Configuration
12
+ #
13
+ # @yield [TaintedLove::Configuration]
14
+ # @returns [TaintedLove::Configuration]
15
+ def enable!
16
+ configuration = TaintedLove::Configuration.new
17
+
18
+ configuration.logger.info('TaintedLove is enabled')
19
+ configuration.replacers = TaintedLove::Replacer::Base.replacers
20
+ configuration.validators = TaintedLove::Validator::Base.validators
21
+ configuration.reporter = TaintedLove::Reporter::StdoutReporter.new
22
+
23
+ # Allows customization of which replacers/validators should be used
24
+ yield configuration if block_given?
25
+
26
+ @configuration = configuration
27
+
28
+ configuration.replacers.each do |replacer|
29
+ replacer = replacer.new
30
+ replacer.replace! if replacer.should_replace?
31
+ end
32
+
33
+ configuration
34
+ end
35
+
36
+ # Report tainted input
37
+ #
38
+ # @param replacer [Symbol] Replacer reporting the issue
39
+ # @param tainted_input [Object] Tainted object
40
+ # @param tags [Array<Symbol>] Tags to classify the warning
41
+ # @param message [String] Message about the warning
42
+ def report(replacer, tainted_input, tags = [], message = nil)
43
+ warning = TaintedLove::Warning.new
44
+ warning.tainted_input = tainted_input
45
+ warning.stack_trace = TaintedLove::StackTrace.new(Thread.current.backtrace(3))
46
+ warning.replacer = replacer
47
+ warning.tags = tags
48
+ warning.message = message
49
+
50
+ should_remove = @configuration.validators.any? do |validator|
51
+ validator.new.remove?(warning)
52
+ end
53
+
54
+ @configuration.reporter.add_warning(warning) unless should_remove
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module TaintedLove
6
+ class Configuration
7
+ attr_accessor :reporter, :logger, :validators, :replacers
8
+
9
+ def initialize
10
+ @reporter = TaintedLove::Reporter::StdoutReporter.new
11
+ @logger = Logger.new(STDERR)
12
+ @validators = []
13
+ @replacers = []
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TaintedLove
4
+ # Replacer will replace methods to report tainted input and taint values from user input coming
5
+ # from librairies.
6
+ module Replacer
7
+ class Base
8
+ # Determines if the replacer can run in the current context. This would usually check Ruby
9
+ # version or gem versions to see which classes and methods to replace.
10
+ def should_replace?
11
+ true
12
+ end
13
+
14
+ # List of defined replacers
15
+ #
16
+ # @return [Array<Class>]
17
+ def self.replacers
18
+ TaintedLove::Replacer.constants.map do |const|
19
+ cls = TaintedLove::Replacer.const_get(const)
20
+ cls if cls.method_defined?(:replace!)
21
+ end.compact
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TaintedLove
4
+ module Replacer
5
+ class ReplaceActionController < Base
6
+ def should_replace?
7
+ Object.const_defined?('ActionController')
8
+ end
9
+
10
+ def replace!
11
+ TaintedLove.proxy_method('ActionController::Instrumentation', :send_file) do |_, *args|
12
+ TaintedLove.report(
13
+ :ReplaceActionController,
14
+ args.first,
15
+ [:lfi],
16
+ 'Sendfile using tainted file name'
17
+ ) if args.first.tainted?
18
+ end
19
+
20
+ TaintedLove.proxy_method('ActionController::Instrumentation', :render) do |_, *args|
21
+ unless args.empty?
22
+ f = args.first
23
+
24
+ if f.is_a?(Hash)
25
+ if f.key?(:inline) && f[:inline].tainted?
26
+ TaintedLove.report(
27
+ :ReplaceActionController,
28
+ f[:inline],
29
+ [:rce],
30
+ 'render(inline:) using tainted string'
31
+ )
32
+ end
33
+
34
+ if f.key?(:file) && f[:file].tainted?
35
+ TaintedLove.report(
36
+ :ReplaceActionController,
37
+ f[:file],
38
+ [:lfi],
39
+ 'render(file:) using tainted file name'
40
+ )
41
+ end
42
+ end
43
+
44
+ if f.is_a?(String) && f.tainted?
45
+ TaintedLove.report(
46
+ :ReplaceActionController,
47
+ f,
48
+ [:lfi],
49
+ 'render using tainted template name'
50
+ )
51
+ end
52
+ end
53
+ end
54
+
55
+ TaintedLove.proxy_method('ActionController::Base', :redirect_to) do |_, *args|
56
+ TaintedLove.report(:ReplaceActionController, args.first, [:open_redirect]) if args.first.tainted?
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TaintedLove
4
+ module Replacer
5
+ class ReplaceActionView < Base
6
+ def should_replace?
7
+ Object.const_defined?('ActionView')
8
+ end
9
+
10
+ def replace!
11
+ ActionView::OutputBuffer.class_eval do
12
+ def append=(value)
13
+ if value.tainted? && value.html_safe?
14
+ TaintedLove.report(
15
+ :ReplaceActionView,
16
+ value,
17
+ [:xss],
18
+ 'Tainted string is html_safe'
19
+ )
20
+ end
21
+
22
+ self << value
23
+ end
24
+ end
25
+
26
+ # Untaint the yield of a template
27
+ mod = Module.new do
28
+ def render(*args, &block)
29
+ super(*args) do |*sub_args, &sub_block|
30
+ block.call(*sub_args, &sub_block).untaint
31
+ end
32
+ end
33
+ end
34
+
35
+ ActionView::Template.prepend(mod) if Object.const_defined?('ActionView::Template')
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TaintedLove
4
+ module Replacer
5
+ class ReplaceActiveRecord < Base
6
+ def should_replace?
7
+ Object.const_defined?('ActiveRecord')
8
+ end
9
+
10
+ def replace!
11
+ require 'active_record/relation'
12
+
13
+ TaintedLove.proxy_method('ActiveRecord::QueryMethods', :where) do |_, *args|
14
+ unless args.empty?
15
+ f = args.first
16
+ if f.is_a?(String) && f.tainted?
17
+ TaintedLove.report(:ReplaceActiveRecord, f, [:sqli], 'Model.where using tainted string')
18
+ end
19
+ end
20
+ end
21
+
22
+ TaintedLove.proxy_method('ActiveRecord::QueryMethods', :select) do |_, *args|
23
+ unless args.empty?
24
+ f = args.first
25
+ if f.is_a?(String) && f.tainted?
26
+ TaintedLove.report(:ReplaceActiveRecord, f, [:sqli], 'Model#select using tainted string')
27
+ end
28
+ end
29
+ end
30
+
31
+ mod = Module.new do
32
+ [:find_by_sql, :count_by_sql].each do |method|
33
+ define_method(method) do |*args|
34
+ if args.first.tainted?
35
+ TaintedLove.report(:ReplaceActiveRecord, args.first, [:sqli], "Model##{method} using tainted string")
36
+ end
37
+
38
+ super(*args)
39
+ end
40
+ end
41
+ end
42
+
43
+ ActiveRecord::Base.extend(mod)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TaintedLove
4
+ module Replacer
5
+ class ReplaceDigest < Base
6
+ def should_replace?
7
+ Object.const_defined?('Digest')
8
+ end
9
+
10
+ def replace!
11
+ digests = [:MD5, :SHA256, :SHA512, :SHA2, :SHA384, :SHA1]
12
+
13
+ digests.each do |digest|
14
+ mod = Digest.const_get(digest)
15
+ mod.extend(create_module(mod)) if Object.const_defined?(mod.to_s)
16
+ end
17
+ end
18
+
19
+ def create_module(digest_class)
20
+ module_code = %{
21
+ Module.new do
22
+ def hexdigest(value)
23
+ digest = super
24
+
25
+ digest.instance_eval do
26
+ def tainted_love_origin
27
+ #{digest_class}
28
+ end
29
+ end
30
+
31
+ digest
32
+ end
33
+ end
34
+ }
35
+ eval(module_code)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TaintedLove
4
+ module Replacer
5
+ class ReplaceFile < Base
6
+ def replace!
7
+ File.instance_eval do
8
+ alias :_tainted_love_original_read :read
9
+ alias :_tainted_love_original_write :write
10
+
11
+ def read(*args)
12
+ if args.first.tainted?
13
+ TaintedLove.report(:ReplaceFile, args.first, [:lfi], 'File read using tainted file name')
14
+
15
+ _tainted_love_original_read(*args)
16
+ else
17
+ _tainted_love_original_read(*args).untaint
18
+ end
19
+ end
20
+
21
+ def write(*args)
22
+ if args.first.tainted?
23
+ TaintedLove.report(:ReplaceFile, args.first, [:lfi], 'File write using tainted file name')
24
+ end
25
+
26
+ _tainted_love_original_write(*args)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TaintedLove
4
+ module Replacer
5
+ class ReplaceKernel < Base
6
+ def replace!
7
+ %i[eval system `].each do |method|
8
+ TaintedLove.proxy_method(Kernel, method) do |_, *args|
9
+ TaintedLove.report(
10
+ :ReplaceKernel,
11
+ args.first,
12
+ [:rce],
13
+ 'Command execution using tainted input'
14
+ ) if args.first&.tainted?
15
+ end
16
+ end
17
+
18
+ Kernel.class_eval do
19
+ alias_method :_tainted_love_original_open, :open
20
+
21
+ def open(*args, &block)
22
+ first = args.first
23
+ return_value = _tainted_love_original_open(*args, &block)
24
+
25
+ if first.tainted?
26
+ return_value.taint
27
+
28
+ TaintedLove.report(
29
+ :ReplaceKernel,
30
+ first,
31
+ [:rce],
32
+ 'Kernel#open begins with "|" and uses tainted input'
33
+ ) if first.is_a?(String) && first[0] == '|'
34
+ else
35
+ return_value.untaint
36
+ end
37
+
38
+ return_value
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TaintedLove
4
+ module Replacer
5
+ class ReplaceMarshal < Base
6
+ def replace!
7
+ mod = Module.new do
8
+ def load(source, proc = nil)
9
+ TaintedLove.report(:ReplaceMarshal, source, [:rce], 'Marshal.load using tainted input') if source.tainted?
10
+
11
+ super(source, proc)
12
+ end
13
+ end
14
+
15
+ Marshal.singleton_class.prepend(mod)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TaintedLove
4
+ module Replacer
5
+ class ReplaceObject < Base
6
+ def replace!
7
+ mod = Module.new do
8
+ def send(*args, &block)
9
+ if args[0].tainted? && args[1].tainted?
10
+ TaintedLove.report(
11
+ :ReplaceObject,
12
+ args.first,
13
+ [:rce],
14
+ 'User input in the first 2 arguments of Object#send'
15
+ )
16
+ end
17
+
18
+ super(*args, &block)
19
+ end
20
+
21
+ def tainted_love_tracking
22
+ @tainted_love_tracking ||= []
23
+ end
24
+ end
25
+
26
+ Object.prepend(mod)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TaintedLove
4
+ module Replacer
5
+ # Ensures user input is tainted in Rails
6
+ class ReplaceRailsUserInput < Base
7
+ def should_replace?
8
+ Object.const_defined?('Rails')
9
+ end
10
+
11
+ def replace!
12
+ # taint headers
13
+ TaintedLove.proxy_method('ActionDispatch::Http::Headers', :[]) do |return_value, *_args|
14
+ return_value.taint
15
+ end
16
+
17
+ # taint the values loaded from the database
18
+ if Object.const_defined?('ActiveRecord::Base')
19
+ ActiveRecord::Base.after_find do
20
+ attributes.values.each do |value|
21
+ value.taint unless value.frozen?
22
+ end
23
+ end
24
+ end
25
+
26
+ if Object.const_defined?('ActionController::Base')
27
+ ActionController::Base.class_eval do
28
+ before_action :taint_params
29
+ before_action :taint_cookies
30
+
31
+ private
32
+
33
+ def taint_params(value = params)
34
+ if value.is_a?(ActionController::Parameters) || value.is_a?(ActiveSupport::HashWithIndifferentAccess)
35
+ value.values.map { |x| x.taint unless x.frozen? }
36
+ value.values.each { |x| taint_params(x) }
37
+ else
38
+ value.taint unless value.frozen?
39
+ end
40
+ end
41
+
42
+ def taint_cookies
43
+ request.cookies.values.each(&:taint)
44
+ end
45
+ end
46
+ end
47
+
48
+ # taint params keys
49
+ if Object.const_defined?('ActionController::Parameters')
50
+ ActionController::Parameters.class_eval do
51
+ def keys
52
+ @parameters.keys.map { |key| key.dup.taint }
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end