immunio 0.15.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (157) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +234 -0
  3. data/README.md +147 -0
  4. data/bin/immunio +5 -0
  5. data/lib/immunio.rb +29 -0
  6. data/lib/immunio/agent.rb +260 -0
  7. data/lib/immunio/authentication.rb +96 -0
  8. data/lib/immunio/blocked_app.rb +38 -0
  9. data/lib/immunio/channel.rb +432 -0
  10. data/lib/immunio/cli.rb +39 -0
  11. data/lib/immunio/context.rb +114 -0
  12. data/lib/immunio/errors.rb +43 -0
  13. data/lib/immunio/immunio_ca.crt +45 -0
  14. data/lib/immunio/logger.rb +87 -0
  15. data/lib/immunio/plugins/action_dispatch.rb +45 -0
  16. data/lib/immunio/plugins/action_view.rb +431 -0
  17. data/lib/immunio/plugins/active_record.rb +707 -0
  18. data/lib/immunio/plugins/active_record_relation.rb +370 -0
  19. data/lib/immunio/plugins/authlogic.rb +80 -0
  20. data/lib/immunio/plugins/csrf.rb +24 -0
  21. data/lib/immunio/plugins/devise.rb +40 -0
  22. data/lib/immunio/plugins/environment_reporter.rb +69 -0
  23. data/lib/immunio/plugins/eval.rb +51 -0
  24. data/lib/immunio/plugins/exception_handler.rb +55 -0
  25. data/lib/immunio/plugins/gems_tracker.rb +5 -0
  26. data/lib/immunio/plugins/haml.rb +36 -0
  27. data/lib/immunio/plugins/http_finisher.rb +50 -0
  28. data/lib/immunio/plugins/http_tracker.rb +203 -0
  29. data/lib/immunio/plugins/io.rb +96 -0
  30. data/lib/immunio/plugins/redirect.rb +42 -0
  31. data/lib/immunio/plugins/warden.rb +66 -0
  32. data/lib/immunio/processor.rb +234 -0
  33. data/lib/immunio/rails.rb +26 -0
  34. data/lib/immunio/request.rb +139 -0
  35. data/lib/immunio/rufus_lua_ext/ref.rb +27 -0
  36. data/lib/immunio/rufus_lua_ext/state.rb +157 -0
  37. data/lib/immunio/rufus_lua_ext/table.rb +137 -0
  38. data/lib/immunio/rufus_lua_ext/utils.rb +13 -0
  39. data/lib/immunio/version.rb +5 -0
  40. data/lib/immunio/vm.rb +291 -0
  41. data/lua-hooks/ext/all.c +78 -0
  42. data/lua-hooks/ext/bitop/README +22 -0
  43. data/lua-hooks/ext/bitop/bit.c +189 -0
  44. data/lua-hooks/ext/extconf.rb +38 -0
  45. data/lua-hooks/ext/libinjection/COPYING +37 -0
  46. data/lua-hooks/ext/libinjection/libinjection.h +65 -0
  47. data/lua-hooks/ext/libinjection/libinjection_html5.c +847 -0
  48. data/lua-hooks/ext/libinjection/libinjection_html5.h +54 -0
  49. data/lua-hooks/ext/libinjection/libinjection_sqli.c +2301 -0
  50. data/lua-hooks/ext/libinjection/libinjection_sqli.h +295 -0
  51. data/lua-hooks/ext/libinjection/libinjection_sqli_data.h +9349 -0
  52. data/lua-hooks/ext/libinjection/libinjection_xss.c +531 -0
  53. data/lua-hooks/ext/libinjection/libinjection_xss.h +21 -0
  54. data/lua-hooks/ext/libinjection/lualib.c +109 -0
  55. data/lua-hooks/ext/lpeg/HISTORY +90 -0
  56. data/lua-hooks/ext/lpeg/lpcap.c +537 -0
  57. data/lua-hooks/ext/lpeg/lpcap.h +43 -0
  58. data/lua-hooks/ext/lpeg/lpcode.c +986 -0
  59. data/lua-hooks/ext/lpeg/lpcode.h +34 -0
  60. data/lua-hooks/ext/lpeg/lpeg-128.gif +0 -0
  61. data/lua-hooks/ext/lpeg/lpeg.html +1429 -0
  62. data/lua-hooks/ext/lpeg/lpprint.c +244 -0
  63. data/lua-hooks/ext/lpeg/lpprint.h +35 -0
  64. data/lua-hooks/ext/lpeg/lptree.c +1238 -0
  65. data/lua-hooks/ext/lpeg/lptree.h +77 -0
  66. data/lua-hooks/ext/lpeg/lptypes.h +149 -0
  67. data/lua-hooks/ext/lpeg/lpvm.c +355 -0
  68. data/lua-hooks/ext/lpeg/lpvm.h +58 -0
  69. data/lua-hooks/ext/lpeg/makefile +55 -0
  70. data/lua-hooks/ext/lpeg/re.html +498 -0
  71. data/lua-hooks/ext/lpeg/test.lua +1409 -0
  72. data/lua-hooks/ext/lua-cmsgpack/CMakeLists.txt +45 -0
  73. data/lua-hooks/ext/lua-cmsgpack/README.md +115 -0
  74. data/lua-hooks/ext/lua-cmsgpack/lua_cmsgpack.c +957 -0
  75. data/lua-hooks/ext/lua-cmsgpack/test.lua +570 -0
  76. data/lua-hooks/ext/lua-snapshot/LICENSE +7 -0
  77. data/lua-hooks/ext/lua-snapshot/Makefile +12 -0
  78. data/lua-hooks/ext/lua-snapshot/README.md +18 -0
  79. data/lua-hooks/ext/lua-snapshot/dump.lua +15 -0
  80. data/lua-hooks/ext/lua-snapshot/snapshot.c +455 -0
  81. data/lua-hooks/ext/lua/COPYRIGHT +34 -0
  82. data/lua-hooks/ext/lua/lapi.c +1087 -0
  83. data/lua-hooks/ext/lua/lapi.h +16 -0
  84. data/lua-hooks/ext/lua/lauxlib.c +652 -0
  85. data/lua-hooks/ext/lua/lauxlib.h +174 -0
  86. data/lua-hooks/ext/lua/lbaselib.c +659 -0
  87. data/lua-hooks/ext/lua/lcode.c +831 -0
  88. data/lua-hooks/ext/lua/lcode.h +76 -0
  89. data/lua-hooks/ext/lua/ldblib.c +398 -0
  90. data/lua-hooks/ext/lua/ldebug.c +638 -0
  91. data/lua-hooks/ext/lua/ldebug.h +33 -0
  92. data/lua-hooks/ext/lua/ldo.c +519 -0
  93. data/lua-hooks/ext/lua/ldo.h +57 -0
  94. data/lua-hooks/ext/lua/ldump.c +164 -0
  95. data/lua-hooks/ext/lua/lfunc.c +174 -0
  96. data/lua-hooks/ext/lua/lfunc.h +34 -0
  97. data/lua-hooks/ext/lua/lgc.c +710 -0
  98. data/lua-hooks/ext/lua/lgc.h +110 -0
  99. data/lua-hooks/ext/lua/linit.c +38 -0
  100. data/lua-hooks/ext/lua/liolib.c +556 -0
  101. data/lua-hooks/ext/lua/llex.c +463 -0
  102. data/lua-hooks/ext/lua/llex.h +81 -0
  103. data/lua-hooks/ext/lua/llimits.h +128 -0
  104. data/lua-hooks/ext/lua/lmathlib.c +263 -0
  105. data/lua-hooks/ext/lua/lmem.c +86 -0
  106. data/lua-hooks/ext/lua/lmem.h +49 -0
  107. data/lua-hooks/ext/lua/loadlib.c +705 -0
  108. data/lua-hooks/ext/lua/loadlib_rel.c +760 -0
  109. data/lua-hooks/ext/lua/lobject.c +214 -0
  110. data/lua-hooks/ext/lua/lobject.h +381 -0
  111. data/lua-hooks/ext/lua/lopcodes.c +102 -0
  112. data/lua-hooks/ext/lua/lopcodes.h +268 -0
  113. data/lua-hooks/ext/lua/loslib.c +243 -0
  114. data/lua-hooks/ext/lua/lparser.c +1339 -0
  115. data/lua-hooks/ext/lua/lparser.h +82 -0
  116. data/lua-hooks/ext/lua/lstate.c +214 -0
  117. data/lua-hooks/ext/lua/lstate.h +169 -0
  118. data/lua-hooks/ext/lua/lstring.c +111 -0
  119. data/lua-hooks/ext/lua/lstring.h +31 -0
  120. data/lua-hooks/ext/lua/lstrlib.c +871 -0
  121. data/lua-hooks/ext/lua/ltable.c +588 -0
  122. data/lua-hooks/ext/lua/ltable.h +40 -0
  123. data/lua-hooks/ext/lua/ltablib.c +287 -0
  124. data/lua-hooks/ext/lua/ltm.c +75 -0
  125. data/lua-hooks/ext/lua/ltm.h +54 -0
  126. data/lua-hooks/ext/lua/lua.c +392 -0
  127. data/lua-hooks/ext/lua/lua.def +131 -0
  128. data/lua-hooks/ext/lua/lua.h +388 -0
  129. data/lua-hooks/ext/lua/lua.rc +28 -0
  130. data/lua-hooks/ext/lua/lua_dll.rc +26 -0
  131. data/lua-hooks/ext/lua/luac.c +200 -0
  132. data/lua-hooks/ext/lua/luac.rc +1 -0
  133. data/lua-hooks/ext/lua/luaconf.h +763 -0
  134. data/lua-hooks/ext/lua/luaconf.h.in +724 -0
  135. data/lua-hooks/ext/lua/luaconf.h.orig +763 -0
  136. data/lua-hooks/ext/lua/lualib.h +53 -0
  137. data/lua-hooks/ext/lua/lundump.c +227 -0
  138. data/lua-hooks/ext/lua/lundump.h +36 -0
  139. data/lua-hooks/ext/lua/lvm.c +767 -0
  140. data/lua-hooks/ext/lua/lvm.h +36 -0
  141. data/lua-hooks/ext/lua/lzio.c +82 -0
  142. data/lua-hooks/ext/lua/lzio.h +67 -0
  143. data/lua-hooks/ext/lua/print.c +227 -0
  144. data/lua-hooks/ext/luautf8/README.md +152 -0
  145. data/lua-hooks/ext/luautf8/lutf8lib.c +1274 -0
  146. data/lua-hooks/ext/luautf8/unidata.h +3064 -0
  147. data/lua-hooks/lib/boot.lua +254 -0
  148. data/lua-hooks/lib/encode.lua +4 -0
  149. data/lua-hooks/lib/lexers/LICENSE +21 -0
  150. data/lua-hooks/lib/lexers/bash.lua +134 -0
  151. data/lua-hooks/lib/lexers/bash_dqstr.lua +62 -0
  152. data/lua-hooks/lib/lexers/css.lua +216 -0
  153. data/lua-hooks/lib/lexers/html.lua +106 -0
  154. data/lua-hooks/lib/lexers/javascript.lua +68 -0
  155. data/lua-hooks/lib/lexers/lexer.lua +1575 -0
  156. data/lua-hooks/lib/lexers/markers.lua +33 -0
  157. metadata +308 -0
@@ -0,0 +1,39 @@
1
+ require 'thor'
2
+ require 'fileutils'
3
+
4
+ module Immunio
5
+ class CLI < Thor
6
+ desc 'init', 'Initializes an Immunio configuration in the current app'
7
+ method_option :key,
8
+ type: :string,
9
+ desc: 'The key generated for your app in Immunio'
10
+ method_option :secret,
11
+ type: :string,
12
+ desc: 'The secret generated for your app in Immunio'
13
+ def init
14
+ if File.exist?(config_file) && File.read(config_file) =~ /key:\s+\w+|secret:\s+\w+/
15
+ say 'Immunio already initialized.', :green
16
+ Kernel.exit 0
17
+ end
18
+
19
+ key = options[:key] || ask('Enter the key generated for your app in Immunio:')
20
+ secret = options[:secret] || ask('Enter the secret generated for your app in Immunio:')
21
+
22
+ FileUtils.mkdir_p(File.dirname(config_file))
23
+
24
+ File.open(config_file, 'a') do |f|
25
+ f.puts "key: #{key}"
26
+ f.puts "secret: #{secret}"
27
+ end
28
+
29
+ say "Credentials written to #{config_file}", :green
30
+ end
31
+
32
+ private
33
+
34
+ def config_file
35
+ root = defined?(Rails) ? Rails.root : Dir.pwd
36
+ File.join(root, 'config', 'immunio.yml')
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,114 @@
1
+ module Immunio
2
+ module Context
3
+ RAILS_TEMPLATE_FILTER = Regexp.new("(.*(_erb|_haml))__+\\d+_\\d+(.*)")
4
+ # Cache for contexts (named in tribute to our buddy Adam Back who invented proof of work)
5
+ @@hash_cache = {}
6
+
7
+ # Calculate context hashes and a stack trace. Additional data, in the form
8
+ # of a String, may be provided to mix into the strict context hash.
9
+ def self.context(additional_data=nil)
10
+ # We can filter out at least the top two frames
11
+ cache_key = Digest::SHA1.hexdigest(caller(2).join())
12
+ if @@hash_cache.has_key?(cache_key) then
13
+ loose_context = @@hash_cache[cache_key]["loose_context"]
14
+ strict_context = @@hash_cache[cache_key]["strict_context"]
15
+ stack = @@hash_cache[cache_key]["stack"]
16
+ loose_stack = @@hash_cache[cache_key]["stack"]
17
+
18
+ if Immunio.agent.config.log_context_data
19
+ Immunio.logger.info {"Stack contexts from cache"}
20
+ end
21
+ else
22
+ # Use ropes as they're faster than string concatenation
23
+ loose_stack_rope = []
24
+ loose_context_rope = []
25
+ stack_rope = []
26
+ strict_context_rope = []
27
+
28
+ # drop the top frame as it's us, but retain the rest. Immunio frames
29
+ # are filtered by the Gem regex.
30
+ locations = caller(1).map do |frame|
31
+ frame = frame.split(":", 3)
32
+ {path: frame[0], line: frame[1], label: frame[2]}
33
+ end
34
+
35
+ locations.each do |frame|
36
+
37
+ # Filter frame names from template rendering to remove generated random bits
38
+ matchdata = RAILS_TEMPLATE_FILTER.match(frame[:label])
39
+ if matchdata != nil then
40
+ frame[:label] = matchdata[1] + matchdata[3]
41
+ end
42
+
43
+ # Reduce paths to be relative to root if possible, to allow
44
+ # relocation. If there's no rails root, or the path doesn't start with
45
+ # the rails root, just use the filename part.
46
+ if defined?(Rails) && defined?(Rails.root) &&
47
+ Rails.root && frame[:path].start_with?(Rails.root.to_s)
48
+ strict_path = frame[:path].sub(Rails.root.to_s, '')
49
+ else
50
+ strict_path = File.basename(frame[:path])
51
+ end
52
+
53
+ stack_rope << "\n" unless stack_rope.empty?
54
+ stack_rope << frame[:path]
55
+ stack_rope << ":"
56
+ stack_rope << frame[:line]
57
+ stack_rope << ":"
58
+ stack_rope << frame[:label]
59
+
60
+ strict_context_rope << "\n" unless strict_context_rope.empty?
61
+ strict_context_rope << strict_path
62
+ strict_context_rope << ":"
63
+ strict_context_rope << frame[:line]
64
+ strict_context_rope << ":"
65
+ strict_context_rope << frame[:label]
66
+
67
+ # Remove pathname from the loose context. The goal here is to prevent
68
+ # upgrading gem versions from changing the loose context key, so for instance
69
+ # users don't have to rebuild their whitelists every time they update a gem
70
+ loose_context_rope << "\n" unless loose_context_rope.empty?
71
+ loose_context_rope << File.basename(frame[:path])
72
+ loose_context_rope << ":"
73
+ loose_context_rope << frame[:label]
74
+
75
+ # build a second seperate rope for the stack that determines ou loose context key
76
+ # This includes filenames for usability -- just method names not being very good
77
+ # for display purposes...
78
+ loose_stack_rope << "\n" unless loose_stack_rope.empty?
79
+ loose_stack_rope << frame[:path]
80
+ loose_stack_rope << ":"
81
+ loose_stack_rope << frame[:label]
82
+
83
+ end
84
+ stack = stack_rope.join()
85
+ strict_stack = strict_context_rope.join()
86
+ loose_stack = loose_stack_rope.join()
87
+
88
+ if Immunio.agent.config.log_context_data
89
+ Immunio.logger.info {"Strict context stack:\n#{strict_stack}"}
90
+ Immunio.logger.info {"Loose context stack:\n#{loose_stack}"}
91
+ end
92
+
93
+ strict_context = Digest::SHA1.hexdigest(strict_stack)
94
+ loose_context = Digest::SHA1.hexdigest(loose_context_rope.join())
95
+ @@hash_cache[cache_key] = {
96
+ "strict_context" => strict_context,
97
+ "loose_context" => loose_context,
98
+ "stack" => stack,
99
+ "loose_stack" => loose_stack
100
+ }
101
+ end
102
+
103
+ # Mix in additional context data
104
+ unless additional_data.nil?
105
+ if Immunio.agent.config.log_context_data
106
+ Immunio.logger.info {"Additional context data:\n#{additional_data}"}
107
+ end
108
+ strict_context = Digest::SHA1.hexdigest(strict_context + additional_data)
109
+ end
110
+
111
+ return strict_context, loose_context, stack, loose_stack
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,43 @@
1
+ require 'singleton'
2
+
3
+ module Immunio
4
+ # General agent error
5
+ class Error < RuntimeError; end
6
+
7
+ # Error to block a request in progress
8
+ # Ruby's `SecurityError` is outside the `StandardError` hierarchy, so it will
9
+ # only be caught if somebody decides to catch it explicitly.
10
+ #
11
+ # `StandardError` and `RuntimeError` are both caught by a default rescue
12
+ # block. This makes it too easy for a developer to catch and ignore one of
13
+ # our blocking errors unintentionally.
14
+ class BlockError < SecurityError; end
15
+
16
+ # Request was not allowed by hook
17
+ class RequestBlocked < BlockError
18
+ class Result
19
+ include Singleton
20
+
21
+ # PG activerecord exception results must have an error_field method before Rails 4.x
22
+ def error_field(_field)
23
+ ""
24
+ end
25
+ end
26
+
27
+ # PG activerecord exceptions must have a result method before Rails 4.x
28
+ def result
29
+ Result.instance
30
+ end
31
+ end
32
+
33
+ # Response overridden by hook
34
+ class OverrideResponse < BlockError
35
+ attr_reader :status, :headers, :body
36
+
37
+ def initialize(status, headers, body)
38
+ @status = status
39
+ @headers = headers
40
+ @body = body
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,45 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh
3
+ MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
4
+ d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD
5
+ QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT
6
+ MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
7
+ b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG
8
+ 9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB
9
+ CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97
10
+ nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt
11
+ 43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P
12
+ T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4
13
+ gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO
14
+ BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR
15
+ TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw
16
+ DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr
17
+ hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg
18
+ 06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF
19
+ PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls
20
+ YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk
21
+ CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4=
22
+ -----END CERTIFICATE-----
23
+
24
+ -----BEGIN CERTIFICATE-----
25
+ MIIDljCCAn6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBcMQswCQYDVQQGEwJDQTEW
26
+ MBQGA1UEChMNSW1tdW4uaW8gSW5jLjEVMBMGA1UECxMMd3d3LmltbXVuLmlvMR4w
27
+ HAYDVQQDExVJbW11bi5pbyBJbmMuIFJvb3QgQ0EwHhcNMTQwMzI1MTQ0NTI3WhcN
28
+ MjQwMzIyMTQ0NTI3WjBcMQswCQYDVQQGEwJDQTEWMBQGA1UEChMNSW1tdW4uaW8g
29
+ SW5jLjEVMBMGA1UECxMMd3d3LmltbXVuLmlvMR4wHAYDVQQDExVJbW11bi5pbyBJ
30
+ bmMuIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDomhh6
31
+ lGHL6IsOlK7TmFikinZ0ShPQQ8WHFbwoLiELGJSNGFKdiSnQICOkjTI6kdXCiOgk
32
+ TYARs8Ty7e1rbCTiBZUV2d2Q1qktS+wHv6GYEukjkX+/yLLf65XsNQlbPheFuBCG
33
+ Tvy8qU8PbdXQu35zLuCwpq8DAanCEXWANOAIYXOaIa0DMqRrVG5QeSfi5mxeXL5q
34
+ Xb4FhqQbMwX7xkQiIB3NQCmVtplDSdMPfCvg/T97rC14XR8jKteRe2OkaLFeGHZp
35
+ mROT2k4dTY4r8h2dV3EW8N4pQizDVpUfzNrgwoaKGccg6JiLLVeMQT7BJxjEi3Tt
36
+ tSt7gf0cWGwMWHbbAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/
37
+ BAQDAgEGMB0GA1UdDgQWBBRUsJtzljk4lIWaEpQXfTR48b70SDAfBgNVHSMEGDAW
38
+ gBRUsJtzljk4lIWaEpQXfTR48b70SDANBgkqhkiG9w0BAQUFAAOCAQEAS0PyWyXj
39
+ 1oF/pWKhY6g71UlcnnISXoutIEQxK0cxqzwjCd/62W5SlgxpSFHJmeuhMHKgT1XF
40
+ PgretLnGSXft1ZmpAtUmHXJYxSFbHz8kv/S5rYW7GeJRTUEqJNGly9nPiAkbY2Uk
41
+ nZSF2uY62CztoDTtESunAcSPN3BxoEEQ3GLg+PFJVFVApyXfjdK5Jc9lfMGH6vIX
42
+ hnW1BIYyyHqaerL2VSFJBaxqDk9NeUmt3w77EdJ0CpRo03Gbw8g19aIrXr9u25lW
43
+ Zm2+58fpRuZ2pdclnTWfjyKb00WCkqfInFLeMvXIEhA60TirhlPBRZ5+8D42I5Sd
44
+ AcWwGci7HgH1+A==
45
+ -----END CERTIFICATE-----
@@ -0,0 +1,87 @@
1
+ require 'logger'
2
+ require 'stringio'
3
+
4
+ module Immunio
5
+ # Subclass global Logger class to add TRACE level
6
+ class Logger < ::Logger
7
+ module Severity
8
+ TRACE = -1
9
+ end
10
+ include Severity
11
+
12
+ def trace?; @level <= TRACE; end
13
+
14
+ def trace(progname = nil, &block)
15
+ add(TRACE, nil, progname, &block)
16
+ end
17
+
18
+ def format_severity(severity)
19
+ SEV_LABEL[severity] || 'ANY'
20
+ end
21
+
22
+ private
23
+ SEV_LABEL = Array.new(::Logger::SEV_LABEL)
24
+ SEV_LABEL[-1] = 'TRACE'
25
+ end
26
+
27
+ attr_reader :logger
28
+
29
+ def self.create_startup_logger
30
+ @startup_messages = StringIO.new
31
+ @logger = Logger.new @startup_messages
32
+
33
+ setup_logger_formatter
34
+ end
35
+
36
+ def self.setup_logger_formatter
37
+ logger.formatter = proc do |severity, datetime, _progname, msg|
38
+ "[#{datetime}] #{severity}: #{msg}\n"
39
+ end
40
+ end
41
+
42
+ def self.switch_to_real_logger(log_file, log_level)
43
+ # Have we already switched to real logger?
44
+ return if !defined?(@startup_messages)
45
+
46
+ if log_file == "STDOUT"
47
+ @logger = Logger.new $stdout
48
+ elsif log_file == "STDERR"
49
+ @logger = Logger.new $stderr
50
+ else
51
+ path = Pathname.new(log_file)
52
+ begin
53
+ FileUtils.mkdir_p path.dirname unless File.exist? path.dirname
54
+
55
+ file = File.open path, 'a'
56
+ file.binmode
57
+ file.sync = true
58
+
59
+ @logger = Logger.new file
60
+ log_file = path.realpath
61
+ rescue StandardError => e
62
+ logger.warn "Failed to open #{log_file} (#{path.realdirpath}) for logging (#{e.message})"
63
+ @logger = Logger.new $stderr
64
+ log_file = "STDERR"
65
+ end
66
+ end
67
+
68
+ # Dump saved log messages during startup to real log
69
+ logger << @startup_messages.string
70
+ remove_instance_variable(:@startup_messages)
71
+
72
+ setup_logger_formatter
73
+
74
+ begin
75
+ logger.level = Logger.const_get(log_level.to_s.upcase)
76
+ rescue
77
+ logger.level = Logger::DEBUG
78
+ logger.debug "Failed to interpret log level #{log_level}, falling back to debug"
79
+ end
80
+
81
+ logger.debug "Logging to #{log_file}"
82
+ end
83
+
84
+ def self.logger
85
+ @logger
86
+ end
87
+ end
@@ -0,0 +1,45 @@
1
+ module Immunio
2
+ module CookieHooks
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ # TODO: should anything be checked to make sure @parent_jar exists
7
+ if method_defined? :[] # Not sure when this wouldn't exist.
8
+ # The following won't work because of the names:
9
+ # alias_method_chain :[], :immunio if method_defined? :[]
10
+ alias_method :lookup_without_immunio, :[]
11
+ alias_method :[], :lookup_with_immunio
12
+ end
13
+
14
+ end
15
+
16
+ def lookup_with_immunio(name)
17
+ Request.time "plugin", "#{Module.nesting[0]}::#{__method__}" do
18
+ raw_cookie_value = @parent_jar[name]
19
+ cookie_value = Request.pause "plugin", "#{Module.nesting[0]}::#{__method__}" do
20
+ lookup_without_immunio(name)
21
+ end
22
+ if !raw_cookie_value.nil? and cookie_value.nil?
23
+ Immunio.run_hook! "action_dispatch", "bad_cookie", key: name,
24
+ value: raw_cookie_value
25
+ end
26
+ cookie_value
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ class ActionDispatch::Cookies
33
+ if defined? SignedCookieJar
34
+ SignedCookieJar.send :include, Immunio::CookieHooks
35
+ end
36
+ if defined? UpgradeLegacySignedCookieJar
37
+ UpgradeLegacySignedCookieJar.send :include, Immunio::CookieHooks
38
+ end
39
+ if defined? EncryptedCookieJar
40
+ EncryptedCookieJar.send :include, Immunio::CookieHooks
41
+ end
42
+ if defined? UpgradeLegacyEncryptedCookieJar
43
+ UpgradeLegacyEncryptedCookieJar.send :include, Immunio::CookieHooks
44
+ end
45
+ end
@@ -0,0 +1,431 @@
1
+ # Hook into ActionView rendering to inject Immunio's hooks.
2
+ require 'securerandom'
3
+
4
+ module Immunio
5
+ # Renders templates by filtering them through Immunio's hook handlers.
6
+ class Template
7
+ attr_accessor :vars
8
+
9
+ def initialize(template)
10
+ @template = template
11
+ @next_var_id = 0
12
+ @next_template_id = 0
13
+ @vars = {}
14
+ @scheduled_fragments_writes = []
15
+ end
16
+
17
+ def id
18
+ (@template.respond_to?(:virtual_path) && @template.virtual_path) || (@template.respond_to?(:source) && @template.source)
19
+ end
20
+
21
+ def ==(other)
22
+ self.class === other && id == other.id
23
+ end
24
+
25
+ def has_source?
26
+ @template.respond_to?(:source) && !@template.source.nil?
27
+ end
28
+
29
+ def is_text?
30
+ @template.formats.first == :text
31
+ end
32
+
33
+ def load_source(context)
34
+ return if !@template.respond_to?(:source) || !@template.source.nil?
35
+
36
+ # @template is a virtual template that doesn't contain the source. We need
37
+ # to try to load the source. But, the virtual template doesn't know the
38
+ # original format of the source template file. Grab the original format
39
+ # from the view context and override the default of just :html when
40
+ # when looking up the template.
41
+ old_formats = context.lookup_context.formats
42
+ begin
43
+ context.lookup_context.formats = @template.formats
44
+ refreshed = @template.refresh(context)
45
+ ensure
46
+ context.lookup_context.formats = old_formats
47
+ end
48
+
49
+ return if refreshed.nil?
50
+
51
+ @template.instance_variable_set :@source, refreshed.source
52
+ end
53
+
54
+ def template_sha
55
+ # A template might have a source but it might be nil.
56
+ @template_sha ||= begin
57
+ Digest::SHA1.hexdigest(@template.source) if has_source?
58
+ end
59
+ end
60
+
61
+ def compiled?
62
+ @template.instance_variable_get :@compiled
63
+ end
64
+
65
+ # Generate the next var unique ID to be used in a template.
66
+ def next_var_id
67
+ id = @next_var_id
68
+ @next_var_id += 1
69
+ id
70
+ end
71
+
72
+ def next_template_id
73
+ id = @next_template_id
74
+ @next_template_id += 1
75
+ id
76
+ end
77
+
78
+ def get_nonce
79
+ # Generate a two byte CSRNG nonce to make our substitutions unpreictable
80
+ # Why only 2 bytes? The nonce is per render, so the odds of guessing it are very low
81
+ # and entropy is finite so we don't want to drain the random pool unnecessarily
82
+ @nonce ||= SecureRandom.hex(2)
83
+ end
84
+
85
+ def mark_var(content, code, template_id, file, line, escape)
86
+ id = Template.next_var_id
87
+ nonce = Template.get_nonce
88
+ Template.vars[id.to_s] = {
89
+ template_sha: template_sha,
90
+ template_id: template_id.to_s,
91
+ nonce: nonce,
92
+ code: code,
93
+ file: file,
94
+ line: line
95
+ }
96
+
97
+ rval = ""
98
+ # NOTE: What happens here is pretty funky to preserve the html_safe SafeBuffer behaviour in ruby.
99
+ # If escaped is true we directly concatenate the content between two SafeBuffers. This will cause
100
+ # escaping if content is not itself a SafeBuffer.
101
+ # Otherwise we explicitly convert to a string, and convert that to a SafeBuffer to ensure that
102
+ # for instance no escaping is performed on the contents of a <%== %> Erubis interpolation.
103
+ if escape and not is_text? then
104
+ # explicitly convert (w/ escapes) and mark safe things that aren't String (SafeBuffer is_a String also)
105
+ # `to_s` is used to render any object passed to a template.
106
+ # It is called internally when appending to ActionView::OutputBuffer.
107
+ # We force rendering to get the actual string.
108
+ # This has no impact if `rendered` is already a string.
109
+ content = content.to_s.html_safe unless content.is_a? String
110
+ # As a failsafe, just return the content if it already contains our markers. This can occur when
111
+ # a helper calls render partial to generate a component of a page. Both render calls are root level
112
+ # templates from our perspective.
113
+ if content =~ /\{immunio-var:\d+:#{nonce}\}/ then
114
+ # don't add markers.
115
+ Immunio.logger.debug {"WARNING: ActionView not marking interpolation which already contains markers: \"#{content}\""}
116
+ rval = content
117
+ else
118
+ rval = "{immunio-var:#{id}:#{nonce}}".html_safe + content + "{/immunio-var:#{id}:#{nonce}}".html_safe
119
+ end
120
+ else
121
+ content = "" if content.nil?
122
+ # See comment above
123
+ if content =~ /\{immunio-var:\\d+:#{nonce}\}/ then
124
+ # don't add markers.
125
+ Immunio.logger.debug {"WARNING: ActionView not marking interpolation which already contains markers: \"#{content}\""}
126
+ rval = content.html_safe
127
+ else
128
+ rval = "{immunio-var:#{id}:#{nonce}}".html_safe + content.html_safe + "{/immunio-var:#{id}:#{nonce}}".html_safe
129
+ end
130
+ end
131
+ rval
132
+ end
133
+
134
+ def mark_and_defer_fragment_write(key, content, options)
135
+ id = @scheduled_fragments_writes.size
136
+ nonce = Template.get_nonce
137
+ @scheduled_fragments_writes << [key, content, options]
138
+ "{immunio-fragment:#{id}:#{nonce}}#{content}{/immunio-fragment:#{id}:#{nonce}}"
139
+ end
140
+
141
+ def render(context)
142
+ load_source context
143
+ # Don't handle templates with no source (inline text templates).
144
+ if not has_source? then
145
+ rendered = yield
146
+ rendered.instance_variable_set("@__immunio_processed", true)
147
+ return rendered
148
+ end
149
+
150
+ begin
151
+ root = true if rendering_stack.length == 0
152
+
153
+ rendering_stack.push self
154
+ # Calculate SHA1 of this template.
155
+ template_sha
156
+ Immunio.logger.debug {"ActionView rendering template with sha #{@template_sha}, root: #{root}"}
157
+ rendered = yield
158
+ rendered.instance_variable_set("@__immunio_processed", true)
159
+
160
+ if root
161
+ # This is the root template. Let ActionView render it, and then look
162
+ # for XSS.
163
+ rendered = rendered.to_str
164
+ # Rendering done!
165
+ result = run_hook! "template_render_done", rendered: rendered, vars: @vars
166
+
167
+ # We use the return value from the hook handler if present.
168
+ rendered = result.fetch("rendered") { rendered.dup }
169
+
170
+ remove_var_markers! rendered
171
+
172
+ # If some fragments were marked to be cached, commit their content to cache.
173
+ write_and_remove_fragments! context, rendered
174
+
175
+ rendered.html_safe
176
+ else
177
+ # This is a partial template. Just render it.
178
+ rendered
179
+ end
180
+ ensure
181
+ top_template = rendering_stack.pop
182
+ unless top_template == self
183
+ raise Error, "Unexpected Immunio::Template on rendering stack. Expected #{id}, got #{top_template.try :id}."
184
+ end
185
+ end
186
+ end
187
+
188
+ # Generate code injected in templates to wrap everything inside `<%= ... %>`.
189
+ def self.generate_render_var_code(code, escape)
190
+ template = Template.current
191
+ if template
192
+ template_id = template.next_template_id
193
+ "(__immunio_result = (#{code}); Immunio::Template.render_var(#{code.strip.inspect}, __immunio_result, #{template_id}, __FILE__, __LINE__, #{escape}))"
194
+ else
195
+ code
196
+ end
197
+ end
198
+
199
+ def self.render_var(code, rendered, template_id, file, line, escape)
200
+ if rendered.instance_variable_get("@__immunio_processed") then
201
+ # Ignore buffers marked as __immunio_processed in render as these are full templates or partials
202
+ return rendered
203
+ elsif code =~ /yield( .*)?/
204
+ # Ignore yielded blocks inside layouts
205
+ return rendered
206
+ end
207
+ template = Template.current
208
+ if template
209
+ rendered = template.mark_var rendered, code, template_id, file, line, escape
210
+ end
211
+ rendered.html_safe
212
+ end
213
+
214
+ def self.current
215
+ rendering_stack.last
216
+ end
217
+
218
+ def self.next_var_id
219
+ rendering_stack.first.next_var_id
220
+ end
221
+
222
+ def self.vars
223
+ rendering_stack.first.vars
224
+ end
225
+
226
+ def self.get_nonce
227
+ rendering_stack.first.get_nonce
228
+ end
229
+
230
+ # Save fragment info to the root template only
231
+ def self.mark_and_defer_fragment_write(*args)
232
+ rendering_stack.first.mark_and_defer_fragment_write(*args)
233
+ end
234
+
235
+ private
236
+ # Stack of the templates currently being rendered.
237
+ def self.rendering_stack
238
+ Thread.current["immunio.rendering_stack"] ||= []
239
+ end
240
+
241
+ def rendering_stack
242
+ self.class.rendering_stack
243
+ end
244
+
245
+ def run_hook!(name, meta={})
246
+ default_meta = {
247
+ template_sha: template_sha,
248
+ name: (@template.respond_to?(:virtual_path) && @template.virtual_path) || nil,
249
+ origin: @template.identifier,
250
+ nonce: Template.get_nonce
251
+ }
252
+ Immunio.run_hook! "action_view", name, default_meta.merge(meta)
253
+ end
254
+
255
+ def write_and_remove_fragments!(context, content)
256
+ # Rails tests do use the context as the view context sometimes.
257
+ if context.is_a? ActionController::Base
258
+ controller = context
259
+ elsif context.respond_to? :controller
260
+ controller = context.controller
261
+ else
262
+ # Some rails unit tests don't have a controller...
263
+ remove_all_markers! content
264
+ return
265
+ end
266
+
267
+ # Iterate to handle nested fragments. Child fragments have lower ids than their parents.
268
+ nonce = Template.get_nonce
269
+ @scheduled_fragments_writes.each_with_index do |(key, _, options), id|
270
+ # Remove the markers ...
271
+ content.sub!(/\{immunio-fragment:#{id}:#{nonce}\}(.*)\{\/immunio-fragment:#{id}:#{nonce}\}/m) do
272
+ # The escaped content inside the markers ($1), is written to cache.
273
+ output = $1
274
+ remove_all_markers! output
275
+ controller.write_fragment_without_immunio key, output, options
276
+ output
277
+ end
278
+ end
279
+ # To be extra safe strip all markers from content
280
+ remove_all_markers! content
281
+ end
282
+
283
+ def remove_var_markers!(input)
284
+ nonce = Template.get_nonce
285
+ # TODO is this the fastest way to remove the markers? Needs benchmarking ...
286
+ input.gsub!(/\{\/?immunio-var:\d+:#{nonce}\}/, "")
287
+ end
288
+
289
+ def remove_all_markers!(input)
290
+ input.gsub!(/\{\/?immunio-(fragment|var):\d+:[a-zA-Z0-9]+\}/, "")
291
+ end
292
+ end
293
+
294
+ # Regexp to test for blocks (... do) in the Ruby code of templates.
295
+ BLOCK_EXPR = ActionView::Template::Handlers::Erubis::BLOCK_EXPR
296
+
297
+ # Hooks for the ERB template engine.
298
+ # (Default one used in Rails).
299
+ module ErubisHooks
300
+ extend ActiveSupport::Concern
301
+
302
+ included do
303
+ alias_method_chain :add_expr, :immunio
304
+ end
305
+
306
+ def add_expr_with_immunio(src, code, indicator)
307
+ # Wrap expressions in the templates to track their rendered value.
308
+ # Do not wrap expressions with blocks, eg.: <%= form_tag do %>
309
+ # TODO should we support blocks?
310
+ Request.time "plugin", "#{Module.nesting[0]}::#{__method__}" do
311
+ unless code =~ BLOCK_EXPR
312
+ # escape unless we see the == indicator
313
+ escape = !(indicator == '==')
314
+ code = Immunio::Template.generate_render_var_code(code, escape)
315
+ end
316
+ Request.pause "plugin", "#{Module.nesting[0]}::#{__method__}" do
317
+ add_expr_without_immunio(src, code, indicator)
318
+ end
319
+ end
320
+ end
321
+ end
322
+
323
+ # Hooks for the HAML template engine.
324
+ module HamlHooks
325
+ extend ActiveSupport::Concern
326
+
327
+ included do
328
+ alias_method_chain :push_script, :immunio
329
+ end
330
+
331
+ def push_script_with_immunio(code, opts = {}, &block)
332
+ # Wrap expressions in the templates to track their rendered value.
333
+ Request.time "plugin", "#{Module.nesting[0]}::#{__method__}" do
334
+ if code !~ BLOCK_EXPR
335
+ # escape if we're told to by HAML
336
+ code = Immunio::Template.generate_render_var_code(code, opts[:escape_html])
337
+ end
338
+ Request.pause "plugin", "#{Module.nesting[0]}::#{__method__}" do
339
+ push_script_without_immunio(code, opts, &block)
340
+ end
341
+ end
342
+ end
343
+ end
344
+
345
+ # Hook for the `ActionView::TemplateRenderer`. These are called for root
346
+ # templates.
347
+ module TemplateRendererHooks
348
+ extend ActiveSupport::Concern
349
+
350
+ included do
351
+ alias_method_chain :render_template, :immunio
352
+ end
353
+
354
+ def render_template_with_immunio(template, *args)
355
+ Request.time "plugin", "#{Module.nesting[0]}::#{__method__}" do
356
+ renderer = Template.new(template)
357
+
358
+ renderer.render @view do
359
+ Request.pause "plugin", "#{Module.nesting[0]}::#{__method__}" do
360
+ render_template_without_immunio(template, *args)
361
+ end
362
+ end
363
+ end
364
+ end
365
+ end
366
+
367
+ # Hook for the `ActionView::Template`. These are called for non-root
368
+ # templates.
369
+ module TemplateHooks
370
+ extend ActiveSupport::Concern
371
+
372
+ included do
373
+ alias_method_chain :render, :immunio
374
+ end
375
+
376
+ def render_with_immunio(context, *args, &block)
377
+ Request.time "plugin", "#{Module.nesting[0]}::#{__method__}" do
378
+ renderer = Template.new(self)
379
+
380
+ renderer.render context do
381
+ Request.pause "plugin", "#{Module.nesting[0]}::#{__method__}" do
382
+ render_without_immunio(context, *args, &block)
383
+ end
384
+ end
385
+ end
386
+ end
387
+ end
388
+
389
+ # Hook for `ActionController::Caching::Fragments` responsible for handling the `<% cache do %>...` in templates.
390
+ module FragmentCachingHooks
391
+ extend ActiveSupport::Concern
392
+
393
+ included do
394
+ alias_method_chain :write_fragment, :immunio
395
+ end
396
+
397
+ def write_fragment_with_immunio(key, content, options = nil)
398
+ return content unless cache_configured?
399
+
400
+ template = Template.current
401
+ if template
402
+ # We're rendering a template. Defer caching 'till we get the escaped content from the hook handler.
403
+ content = Template.mark_and_defer_fragment_write(key, content, options)
404
+ else
405
+ # Not rendering a template. Ignore.
406
+ # Shouldn't happen. But, just to be safe in case fragment caching is used in the controller for something else.
407
+ content = write_fragment_without_immunio(key, content, options)
408
+ end
409
+
410
+ content
411
+ end
412
+ end
413
+ end
414
+
415
+ # Add XSS hooks if enabled
416
+ if Immunio::agent.plugin_enabled?("xss") then
417
+ # Hook into template engines.
418
+ ActionView::Template::Handlers::Erubis.send :include, Immunio::ErubisHooks
419
+
420
+ ActiveSupport.on_load(:after_initialize) do
421
+ # Wait after Rails initialization to patch custom template engines.
422
+ if defined? Haml::Compiler
423
+ Haml::Compiler.send :include, Immunio::HamlHooks
424
+ end
425
+ end
426
+
427
+ # Hook into rendering process of Rails.
428
+ ActionView::TemplateRenderer.send :include, Immunio::TemplateRendererHooks
429
+ ActionView::Template.send :include, Immunio::TemplateHooks
430
+ ActionController::Caching::Fragments.send :include, Immunio::FragmentCachingHooks
431
+ end