immunio 0.15.2

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 (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