perimeter_x 1.0.6.pre.alpha → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +5 -3
  3. data/Dockerfile +5 -3
  4. data/Gemfile +1 -1
  5. data/Gemfile.lock +44 -2
  6. data/LICENSE.txt +9 -12
  7. data/Rakefile +9 -2
  8. data/changelog.md +20 -0
  9. data/examples/app/controllers/home_controller.rb +9 -0
  10. data/examples/app/views/home/index.html.erb.dist +20 -0
  11. data/examples/config/initializers/perimeterx.rb.dist +8 -0
  12. data/examples/{routes.rb → config/routes.rb} +0 -0
  13. data/lib/perimeter_x.rb +109 -33
  14. data/lib/perimeterx/configuration.rb +25 -17
  15. data/lib/perimeterx/internal/clients/perimeter_x_activity_client.rb +92 -0
  16. data/lib/perimeterx/internal/clients/perimeter_x_risk_client.rb +28 -0
  17. data/lib/perimeterx/internal/exceptions/px_cookie_decryption_exception.rb +5 -0
  18. data/lib/perimeterx/internal/perimeter_x_context.rb +81 -53
  19. data/lib/perimeterx/internal/perimeter_x_cookie.rb +140 -0
  20. data/lib/perimeterx/internal/perimeter_x_cookie_v1.rb +42 -0
  21. data/lib/perimeterx/internal/perimeter_x_cookie_v3.rb +37 -0
  22. data/lib/perimeterx/internal/validators/perimeter_x_captcha_validator.rb +65 -0
  23. data/lib/perimeterx/internal/validators/perimeter_x_cookie_validator.rb +76 -0
  24. data/lib/perimeterx/internal/validators/perimeter_x_s2s_validator.rb +114 -0
  25. data/lib/perimeterx/utils/px_constants.rb +45 -0
  26. data/lib/perimeterx/utils/px_http_client.rb +47 -26
  27. data/lib/perimeterx/utils/px_logger.rb +12 -6
  28. data/lib/perimeterx/utils/px_template_factory.rb +31 -0
  29. data/lib/perimeterx/utils/templates/block.mustache +146 -0
  30. data/lib/perimeterx/utils/templates/captcha.mustache +185 -0
  31. data/lib/perimeterx/version.rb +2 -2
  32. data/perimeter_x.gemspec +6 -1
  33. data/readme.md +218 -34
  34. metadata +90 -11
  35. data/bin/console +0 -14
  36. data/bin/setup +0 -8
  37. data/examples/home_controller.rb.dist +0 -23
  38. data/lib/perimeterx/internal/perimeter_x_risk_client.rb +0 -29
  39. data/lib/perimeterx/internal/perimeter_x_s2s_validator.rb +0 -67
@@ -0,0 +1,92 @@
1
+ require 'perimeterx/internal/clients/perimeter_x_risk_client'
2
+
3
+ module PxModule
4
+ class PerimeterxActivitiesClient < PerimeterxRiskClient
5
+
6
+ attr_accessor :activities
7
+
8
+ def initialize(px_config, http_client)
9
+ super(px_config, http_client)
10
+ @logger.debug("PerimeterxActivitiesClients[initialize]")
11
+ @activities = [];
12
+ end
13
+
14
+ def send_to_perimeterx(activity_type, px_ctx, details = [])
15
+ @logger.debug("PerimeterxActivitiesClients[send_to_perimeterx]")
16
+ @logger.debug("PerimeterxActivitiesClients[send_to_perimeterx]: new activity #{activity_type} logged")
17
+
18
+ if (@px_config.key?(:additional_activity_handler))
19
+ @px_config[:additional_activity_handler].call(activity_type, px_ctx, details)
20
+ end
21
+
22
+ details[:module_version] = @px_config[:sdk_name]
23
+ px_data = {
24
+ :type => activity_type,
25
+ :headers => format_headers(px_ctx),
26
+ :timestamp => (Time.now.to_f*1000).floor,
27
+ :socket_ip => px_ctx.context[:ip],
28
+ :px_app_id => @px_config[:app_id],
29
+ :url => px_ctx.context[:full_url],
30
+ :details => details,
31
+ }
32
+
33
+ if (px_ctx.context.key?(:vid))
34
+ @logger.debug("PerimeterxActivitiesClients[send_to_perimeterx]: found vid in ctx")
35
+ px_data[:vid] = px_ctx.context[:vid]
36
+ end
37
+
38
+ # Prepare request
39
+ headers = {
40
+ "Authorization" => "Bearer #{@px_config[:auth_token]}" ,
41
+ "Content-Type" => "application/json"
42
+ };
43
+
44
+ @activities.push(px_data)
45
+ if (@activities.size == @px_config[:max_buffer_len])
46
+ @logger.debug("PerimeterxActivitiesClients[send_to_perimeterx]: max buffer length reached, sending activities")
47
+ @http_client.async_post(PxModule::API_V1_S2S, @activities, headers)
48
+
49
+ @activities.clear
50
+ end
51
+ end
52
+
53
+ def send_block_activity(px_ctx)
54
+ @logger.debug("PerimeterxActivitiesClients[send_block_activity]")
55
+ if (!@px_config[:send_page_acitivites])
56
+ @logger.debug("PerimeterxActivitiesClients[send_block_activity]: sending activites is disabled")
57
+ return
58
+ end
59
+
60
+ details = {
61
+ :block_uuid => px_ctx.context[:uuid],
62
+ :block_score => px_ctx.context[:score],
63
+ :block_reason => px_ctx.context[:block_reason]
64
+ }
65
+
66
+ send_to_perimeterx(PxModule::BLOCK_ACTIVITY, px_ctx, details)
67
+
68
+ end
69
+
70
+ def send_page_requested_activity(px_ctx)
71
+ @logger.debug("PerimeterxActivitiesClients[send_page_requested_activity]")
72
+ if (!@px_config[:send_page_acitivites])
73
+ return
74
+ end
75
+
76
+ details = {
77
+ :http_version => px_ctx.context[:http_version],
78
+ :http_method => px_ctx.context[:http_method]
79
+ }
80
+
81
+ if (px_ctx.context.key?(:decoded_cookie))
82
+ details[:px_cookie] = px_ctx.context[:decoded_cookie]
83
+ end
84
+
85
+ if (px_ctx.context.key?(:cookie_hmac))
86
+ details[:px_cookie_hmac] = px_ctx.context[:cookie_hmac]
87
+ end
88
+
89
+ send_to_perimeterx(PxModule::PAGE_REQUESTED_ACTIVITY, px_ctx, details)
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,28 @@
1
+ require 'perimeterx/utils/px_logger'
2
+
3
+ module PxModule
4
+ class PerimeterxRiskClient
5
+ attr_accessor :px_config
6
+ attr_accessor :http_client
7
+
8
+ def initialize(px_config, http_client)
9
+ @px_config = px_config
10
+ @http_client = http_client;
11
+ @logger = px_config[:logger]
12
+ end
13
+
14
+ def format_headers(px_ctx)
15
+ @logger.debug("PerimeterxRiskClient[format_headers]")
16
+ formated_headers = []
17
+ px_ctx.context[:headers].each do |k,v|
18
+ if (!@px_config[:sensitive_headers].include? k.to_s)
19
+ formated_headers.push({
20
+ :name => k.to_s,
21
+ :value => v
22
+ })
23
+ end #end if
24
+ end #end forech
25
+ return formated_headers
26
+ end #end method
27
+ end #end class
28
+ end
@@ -0,0 +1,5 @@
1
+ class PxCookieDecryptionException < StandardError
2
+ def initialize(msg)
3
+ super(msg)
4
+ end
5
+ end
@@ -1,62 +1,90 @@
1
1
  require 'perimeterx/utils/px_logger'
2
2
 
3
- class PerimeterXContext
4
- L = PxLogger.instance
5
-
6
- attr_accessor :context
7
- attr_accessor :px_config
8
-
9
- def initialize(px_config, req)
10
- L.info("PerimeterXContext: initialize")
11
- @context = Hash.new
12
-
13
- @context[:px_cookies] = Hash.new
14
- @context[:headers] = Hash.new
15
- cookies = req.cookies
16
- if (!cookies.empty?)
17
- # Prepare hashed cookies
18
- cookies.each do |k, v|
19
- case k
20
- when "_px3"
21
- @context[:px_cookies] = v
22
- when "_px"
23
- @context[:px_cookies] = v
24
- when "_pxCaptcha"
25
- @context[:px_captcha] = v
3
+ module PxModule
4
+ class PerimeterXContext
5
+
6
+ attr_accessor :context
7
+ attr_accessor :px_config
8
+
9
+ def initialize(px_config, req)
10
+ @logger = px_config[:logger];
11
+ @logger.debug("PerimeterXContext[initialize] ")
12
+ @context = Hash.new
13
+
14
+ @context[:px_cookie] = Hash.new
15
+ @context[:headers] = Hash.new
16
+ cookies = req.cookies
17
+ if (!cookies.empty?)
18
+ # Prepare hashed cookies
19
+ cookies.each do |k, v|
20
+ case k.to_s
21
+ when "_px3"
22
+ @context[:px_cookie][:v3] = v
23
+ when "_px"
24
+ @context[:px_cookie][:v1] = v
25
+ when "_pxCaptcha"
26
+ @context[:px_captcha] = v
27
+ end
28
+ end #end case
29
+ end #end empty cookies
30
+
31
+ req.headers.each do |k, v|
32
+ if (k.start_with? "HTTP_")
33
+ header = k.to_s.gsub("HTTP_", "")
34
+ header = header.gsub("_", "-").downcase
35
+ @context[:headers][header.to_sym] = v
26
36
  end
27
- end #end case
28
- end #end empty cookies
29
-
30
- req.headers.each do |k, v|
31
- if (k.start_with? "HTTP_")
32
- header = k.to_s.gsub("HTTP_", "")
33
- header = header.gsub("_", "-").downcase
34
- @context[:headers][header.to_sym] = v
37
+ end #end headers foreach
38
+
39
+ @context[:hostname]= req.server_name
40
+ @context[:user_agent] = req.user_agent ? req.user_agent : ''
41
+ @context[:uri] = px_config[:custom_uri] ? px_config[:custom_uri].call(req) : req.fullpath
42
+ @context[:full_url] = req.original_url
43
+ @context[:format] = req.format.symbol
44
+ @context[:score] = 0
45
+
46
+ if px_config.key?(:custom_user_ip)
47
+ @context[:ip] = req.headers[px_config[:custom_user_ip]]
48
+ elsif px_config.key?(:px_custom_user_ip_method)
49
+ @context[:ip] = px_config[:px_custom_user_ip_method].call(req)
50
+ else
51
+ @context[:ip] = req.ip
35
52
  end
36
- end #end headers foreach
37
-
38
- @context[:hostname]= req.headers['HTTP_HOST']
39
- @context[:user_agent] = req.headers['HTTP_USER_AGENT'] ? req.headers['HTTP_USER_AGENT'] : ''
40
- @context[:full_url] = req.original_url
41
- @context[:score] = 0
42
-
43
- if px_config.key?('custom_user_ip')
44
- @context[:ip] = px_config['custom_user_ip']
45
- elsif px_config.key?('px_custom_user_ip_method')
46
- puts "px_custom_user_ip_method triggered"
47
- @context[:ip] = px_config['px_custom_user_ip_method'].call(req)
48
- else
49
- @context[:ip] = req.headers['REMOTE_ADDR'];
50
- end
51
53
 
52
- if req.headers['SERVER_PROTOCOL']
53
- httpVer = req.headers['SERVER_PROTOCOL'].split("/")
54
- if httpVer.size > 0
55
- @context[:http_version] = httpVer[1];
54
+ if req.server_protocol
55
+ httpVer = req.server_protocol.split("/")
56
+ if httpVer.size > 0
57
+ @context[:http_version] = httpVer[1];
58
+ end
56
59
  end
60
+ @context[:http_method] = req.method
61
+ @context[:sensitive_route] = check_sensitive_route(px_config[:sensitive_routes], @context[:uri])
62
+ end #end init
63
+
64
+ def check_sensitive_route(sensitive_routes, uri)
65
+ sensitive_routes.each do |sensitive_route|
66
+ return true if uri.start_with? sensitive_route
67
+ end
68
+ return false
69
+ end
70
+
71
+ def set_block_action_type(action)
72
+ @context[:block_action] = case action
73
+ when "c"
74
+ "captcha"
75
+ when "b"
76
+ return "block"
77
+ when "j"
78
+ return "challenge"
79
+ else
80
+ return "captcha"
81
+ end
57
82
  end
58
- @context[:http_method] = req.headers['REQUEST_METHOD'];
59
83
 
60
- end #end init
84
+ def get_px_cookie
85
+ cookie = @context[:px_cookie].key?(:v3) ? @context[:px_cookie][:v3] : @context[:px_cookie][:v1]
86
+ return cookie.tr(' ','+') if !cookie.nil?
87
+ end
61
88
 
62
- end #end class
89
+ end
90
+ end
@@ -0,0 +1,140 @@
1
+ require 'active_support/security_utils'
2
+ require 'base64'
3
+ require 'openssl'
4
+ require 'perimeterx/internal/exceptions/px_cookie_decryption_exception'
5
+
6
+ module PxModule
7
+ class PerimeterxCookie
8
+ attr_accessor :px_cookie, :px_config, :px_ctx, :cookie_secret, :decoded_cookie
9
+
10
+ def initialize(px_config)
11
+ @px_config = px_config
12
+ @logger = px_config[:logger]
13
+ end
14
+
15
+ def self.px_cookie_factory(px_ctx, px_config)
16
+ if (px_ctx.context[:px_cookie].key?(:v3))
17
+ return PerimeterxCookieV3.new(px_config, px_ctx)
18
+ end
19
+ return PerimeterxCookieV1.new(px_config, px_ctx)
20
+ end
21
+
22
+ def cookie_score
23
+ #abstract, must be implemented
24
+ raise Exception.new("Unimplemented method")
25
+ end
26
+
27
+ def cookie_hmac
28
+ #abstract, must be implemented
29
+ raise Exception.new("Unimplemented method")
30
+ end
31
+
32
+ def valid_format?(cookie)
33
+ #abstract, must be implemented
34
+ raise Exception.new("Unimplemented method")
35
+ end
36
+
37
+ def cookie_block_action
38
+ #abstract, must be implemented
39
+ raise Exception.new("Unimplemented method")
40
+ end
41
+
42
+ def secured?
43
+ #abstract, must be implemented
44
+ raise Exception.new("Unimplemented method")
45
+ end
46
+
47
+ def is_valid?
48
+ return deserialize && !expired? && secured?
49
+ end
50
+
51
+ def cookie_time
52
+ return @decoded_cookie[:t]
53
+ end
54
+
55
+ def cookie_uuid
56
+ return @decoded_cookie[:u]
57
+ end
58
+
59
+ def cookie_vid
60
+ return @decoded_cookie[:v]
61
+ end
62
+
63
+ def high_score?
64
+ return cookie_score >= @px_config[:blocking_score]
65
+ end
66
+
67
+ def expired?
68
+ return cookie_time < (Time.now.to_f*1000).floor
69
+ end
70
+
71
+
72
+ def deserialize
73
+ if (!@decoded_cookie.nil?)
74
+ return true
75
+ end
76
+
77
+ # Decode or decrypt, depends on configuration
78
+ if (@px_config[:encryption_enabled])
79
+ cookie = decrypt(@px_cookie)
80
+ else
81
+ cookie = decode(@px_cookie)
82
+ end
83
+
84
+ if (cookie.nil?)
85
+ return false
86
+ end
87
+
88
+ if (!valid_format?(cookie))
89
+ return false
90
+ end
91
+
92
+ @decoded_cookie = cookie
93
+
94
+ return true
95
+ end
96
+
97
+
98
+ def decrypt(px_cookie)
99
+ begin
100
+ if (px_cookie.nil?)
101
+ return
102
+ end
103
+ px_cookie = px_cookie.gsub(' ', '+')
104
+ salt, iterations, cipher_text = px_cookie.split(':')
105
+ iterations = iterations.to_i
106
+ salt = Base64.decode64(salt)
107
+ cipher_text = Base64.decode64(cipher_text)
108
+ digest = OpenSSL::Digest::SHA256.new
109
+ value = OpenSSL::PKCS5.pbkdf2_hmac(@px_config[:cookie_key], salt, iterations, 48, digest)
110
+ key = value[0..31]
111
+ iv = value[32..-1]
112
+ cipher = OpenSSL::Cipher::AES256.new(:CBC)
113
+ cipher.decrypt
114
+ cipher.key = key
115
+ cipher.iv = iv
116
+ plaintext = cipher.update(cipher_text) + cipher.final
117
+
118
+ return eval(plaintext)
119
+ rescue Exception => e
120
+ @logger.debug("PerimeterxCookie[decrypt]: Cookie decrypt fail #{e.message}")
121
+ raise PxCookieDecryptionException.new("Cookie decrypt fail => #{e.message}");
122
+ end
123
+ end
124
+
125
+ def decode(px_cookie)
126
+ return eval(Base64.decode64(px_cookie))
127
+ end
128
+
129
+
130
+ def hmac_valid?(hmac_str, cookie_hmac)
131
+ hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, @cookie_secret, hmac_str)
132
+ # ref: https://thisdata.com/blog/timing-attacks-against-string-comparison/
133
+ password_correct = ActiveSupport::SecurityUtils.secure_compare(
134
+ ::Digest::SHA256.hexdigest(cookie_hmac),
135
+ ::Digest::SHA256.hexdigest(hmac)
136
+ )
137
+
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,42 @@
1
+ module PxModule
2
+ class PerimeterxCookieV1 < PerimeterxCookie
3
+
4
+ attr_accessor :px_config, :px_ctx
5
+
6
+ def initialize(px_config, px_ctx)
7
+ super(px_config)
8
+ @px_ctx = px_ctx
9
+ @px_cookie = px_ctx.get_px_cookie
10
+ @cookie_secret = px_config[:cookie_key]
11
+ @logger.debug("PerimeterxCookieV1[initialize]")
12
+ end
13
+
14
+ def cookie_score
15
+ return @decoded_cookie[:s][:b]
16
+ end
17
+
18
+ def cookie_hmac
19
+ return @decoded_cookie[:h]
20
+ end
21
+
22
+ def valid_format?(cookie)
23
+ return cookie.key?(:t) && cookie.key?(:s) && cookie[:s].key?(:b) && cookie.key?(:s) && cookie.key?(:v) && cookie.key?(:h)
24
+ end
25
+
26
+ def cookie_block_action
27
+ return 'c'
28
+ end
29
+
30
+ def secured?
31
+ base_hmac_str = "#{cookie_time}#{@decoded_cookie[:s][:a]}#{cookie_score}#{cookie_uuid}#{cookie_vid}"
32
+
33
+ hmac_str_withip = "#{base_hmac_str}#{@px_ctx.context[:ip]}#{@px_ctx.context[:user_agent]}"
34
+
35
+ hmac_str_withoutip = "#{base_hmac_str}#{@px_ctx.context[:user_agent]}"
36
+
37
+ return (hmac_valid?(hmac_str_withoutip, cookie_hmac) || hmac_valid?(hmac_str_withip, cookie_hmac))
38
+ end
39
+
40
+ end
41
+
42
+ end