perimeter_x 1.0.6.pre.alpha → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +6 -3
  3. data/.travis.yml +3 -0
  4. data/Dockerfile +19 -41
  5. data/Gemfile +1 -1
  6. data/Gemfile.lock +51 -3
  7. data/LICENSE.txt +9 -12
  8. data/Rakefile +10 -2
  9. data/changelog.md +72 -0
  10. data/examples/app/controllers/home_controller.rb +9 -0
  11. data/examples/app/views/home/index.html.erb.dist +20 -0
  12. data/examples/config/initializers/perimeterx.rb.dist +8 -0
  13. data/examples/{routes.rb → config/routes.rb} +0 -0
  14. data/lib/perimeter_x.rb +192 -37
  15. data/lib/perimeterx/configuration.rb +30 -18
  16. data/lib/perimeterx/internal/clients/perimeter_x_activity_client.rb +110 -0
  17. data/lib/perimeterx/internal/clients/perimeter_x_risk_client.rb +28 -0
  18. data/lib/perimeterx/internal/exceptions/px_cookie_decryption_exception.rb +5 -0
  19. data/lib/perimeterx/internal/payload/perimeter_x_cookie_v1.rb +42 -0
  20. data/lib/perimeterx/internal/payload/perimeter_x_cookie_v3.rb +37 -0
  21. data/lib/perimeterx/internal/payload/perimeter_x_payload.rb +148 -0
  22. data/lib/perimeterx/internal/payload/perimeter_x_token_v1.rb +38 -0
  23. data/lib/perimeterx/internal/payload/perimeter_x_token_v3.rb +36 -0
  24. data/lib/perimeterx/internal/perimeter_x_context.rb +112 -53
  25. data/lib/perimeterx/internal/validators/perimeter_x_cookie_validator.rb +103 -0
  26. data/lib/perimeterx/internal/validators/perimeter_x_s2s_validator.rb +128 -0
  27. data/lib/perimeterx/utils/px_constants.rb +62 -0
  28. data/lib/perimeterx/utils/px_http_client.rb +43 -28
  29. data/lib/perimeterx/utils/px_logger.rb +12 -6
  30. data/lib/perimeterx/utils/px_template_factory.rb +41 -0
  31. data/lib/perimeterx/utils/templates/block_template.mustache +175 -0
  32. data/lib/perimeterx/utils/templates/ratelimit.mustache +9 -0
  33. data/lib/perimeterx/version.rb +2 -2
  34. data/perimeter_x.gemspec +10 -4
  35. data/readme.md +258 -42
  36. metadata +130 -24
  37. data/bin/console +0 -14
  38. data/bin/setup +0 -8
  39. data/examples/home_controller.rb.dist +0 -23
  40. data/lib/perimeterx/internal/perimeter_x_risk_client.rb +0 -29
  41. data/lib/perimeterx/internal/perimeter_x_s2s_validator.rb +0 -67
@@ -1,30 +1,42 @@
1
- module PerimeterX
1
+ require 'perimeterx/utils/px_logger'
2
+ require 'perimeterx/utils/px_constants'
3
+
4
+ module PxModule
2
5
  class Configuration
3
6
 
4
7
  attr_accessor :configuration
5
8
  attr_accessor :PX_DEFAULT
6
- attr_accessor :MONITOR_MODE
7
- attr_accessor :ACTIVE_MODE
8
-
9
- MONITOR_MODE = 1
10
- ACTIVE_MODE = 2
11
9
 
12
10
  PX_DEFAULT = {
13
- "app_id" => nil,
14
- "auth_token" => nil,
15
- "module_enabled" => true,
16
- "blocking_score" => 70,
17
- "sensitive_headers" => ["cookie", "cookies"],
18
- "api_connect_timeout" => 1,
19
- "api_timeout" => 1,
20
- "sdk_name" => "RUBY SLIM SDK v1.0.0",
21
- "debug_mode" => false,
22
- "module_mode" => MONITOR_MODE,
11
+ :app_id => nil,
12
+ :cookie_key => nil,
13
+ :auth_token => nil,
14
+ :module_enabled => true,
15
+ :challenge_enabled => true,
16
+ :encryption_enabled => true,
17
+ :blocking_score => 100,
18
+ :sensitive_headers => ["http-cookie", "http-cookies"],
19
+ :api_connect_timeout => 1,
20
+ :api_timeout => 1,
21
+ :max_buffer_len => 10,
22
+ :send_page_activities => true,
23
+ :send_block_activities => true,
24
+ :sdk_name => PxModule::SDK_NAME,
25
+ :debug => false,
26
+ :module_mode => PxModule::MONITOR_MODE,
27
+ :local_proxy => false,
28
+ :sensitive_routes => [],
29
+ :whitelist_routes => [],
30
+ :ip_headers => [],
31
+ :ip_header_function => nil,
32
+ :bypass_monitor_header => nil,
33
+ :risk_cookie_max_iterations => 5000
23
34
  }
24
35
 
25
36
  def initialize(params)
26
- PX_DEFAULT["perimeterx_server_host"] = "https://sapi-#{params['app_id'].downcase}.perimeterx.net"
27
- @configuration = PX_DEFAULT.merge(params);
37
+ PX_DEFAULT[:backend_url] = "https://sapi-#{params[:app_id].downcase}.perimeterx.net"
38
+ @configuration = PX_DEFAULT.merge(params)
39
+ @configuration[:logger] = PxLogger.new(@configuration[:debug])
28
40
  end
29
41
  end
30
42
  end
@@ -0,0 +1,110 @@
1
+ require 'perimeterx/internal/clients/perimeter_x_risk_client'
2
+
3
+ module PxModule
4
+ class PerimeterxActivitiesClient < PerimeterxRiskClient
5
+
6
+
7
+ def initialize(px_config, http_client)
8
+ super(px_config, http_client)
9
+ @logger.debug("PerimeterxActivitiesClients[initialize]")
10
+ end
11
+
12
+ def send_to_perimeterx(activity_type, px_ctx, details = [])
13
+ @logger.debug("PerimeterxActivitiesClients[send_to_perimeterx]")
14
+ @logger.debug("PerimeterxActivitiesClients[send_to_perimeterx]: new activity #{activity_type} logged")
15
+
16
+ if (@px_config.key?(:additional_activity_handler))
17
+ @px_config[:additional_activity_handler].call(activity_type, px_ctx, details)
18
+ end
19
+
20
+ details[:module_version] = @px_config[:sdk_name]
21
+ details[:cookie_origin] = px_ctx.context[:cookie_origin]
22
+
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
+ s = Time.now
45
+ @http_client.async.post(PxModule::API_V1_S2S, px_data, headers)
46
+ e = Time.now
47
+ @logger.debug("PerimeterxActivitiesClients[send_to_perimeterx]: post runtime #{(e-s)*1000}")
48
+ end
49
+
50
+ def send_block_activity(px_ctx)
51
+ @logger.debug("PerimeterxActivitiesClients[send_block_activity]")
52
+ if (!@px_config[:send_block_activities])
53
+ @logger.debug("PerimeterxActivitiesClients[send_block_activity]: sending activites is disabled")
54
+ return
55
+ end
56
+
57
+ details = {
58
+ :http_version => px_ctx.context[:http_version],
59
+ :http_method => px_ctx.context[:http_method],
60
+ :client_uuid => px_ctx.context[:uuid],
61
+ :block_score => px_ctx.context[:score],
62
+ :block_reason => px_ctx.context[:blocking_reason],
63
+ :simulated_block => @px_config[:module_mode] == PxModule::MONITOR_MODE
64
+ }
65
+
66
+ if (px_ctx.context.key?(:risk_rtt))
67
+ details[:risk_rtt] = px_ctx.context[:risk_rtt]
68
+ end
69
+
70
+ if (px_ctx.context.key?(:px_orig_cookie))
71
+ details[:px_orig_cookie] = px_ctx.context[:px_orig_cookie]
72
+ end
73
+
74
+ send_to_perimeterx(PxModule::BLOCK_ACTIVITY, px_ctx, details)
75
+
76
+ end
77
+
78
+ def send_page_requested_activity(px_ctx)
79
+ @logger.debug("PerimeterxActivitiesClients[send_page_requested_activity]")
80
+ if (!@px_config[:send_page_activities])
81
+ return
82
+ end
83
+
84
+ details = {
85
+ :http_version => px_ctx.context[:http_version],
86
+ :http_method => px_ctx.context[:http_method],
87
+ :client_uuid => px_ctx.context[:uuid],
88
+ :pass_reason => px_ctx.context[:pass_reason]
89
+ }
90
+
91
+ if (px_ctx.context.key?(:decoded_cookie))
92
+ details[:px_cookie] = px_ctx.context[:decoded_cookie]
93
+ end
94
+
95
+ if (px_ctx.context.key?(:px_orig_cookie))
96
+ details[:px_orig_cookie] = px_ctx.context[:px_orig_cookie]
97
+ end
98
+
99
+ if (px_ctx.context.key?(:cookie_hmac))
100
+ details[:px_cookie_hmac] = px_ctx.context[:cookie_hmac]
101
+ end
102
+
103
+ if (px_ctx.context.key?(:risk_rtt))
104
+ details[:risk_rtt] = px_ctx.context[:risk_rtt]
105
+ end
106
+
107
+ send_to_perimeterx(PxModule::PAGE_REQUESTED_ACTIVITY, px_ctx, details)
108
+ end
109
+ end
110
+ 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
@@ -0,0 +1,42 @@
1
+ module PxModule
2
+ class PerimeterxCookieV1 < PerimeterxPayload
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
@@ -0,0 +1,37 @@
1
+ module PxModule
2
+ class PerimeterxCookieV3 < PerimeterxPayload
3
+
4
+ attr_accessor :px_config, :px_ctx, :cookie_hash
5
+
6
+ def initialize(px_config, px_ctx)
7
+ super(px_config)
8
+ hash, cookie = px_ctx.get_px_cookie().split(':', 2)
9
+ @px_cookie = cookie
10
+ @cookie_hash = hash
11
+ @px_ctx = px_ctx
12
+ @cookie_secret = px_config[:cookie_key]
13
+ @logger.debug("PerimeterxCookieV3[initialize]")
14
+ end
15
+
16
+ def cookie_score
17
+ return @decoded_cookie[:s]
18
+ end
19
+
20
+ def cookie_hmac
21
+ return @cookie_hash
22
+ end
23
+
24
+ def valid_format?(cookie)
25
+ return cookie.key?(:t) && cookie.key?(:s) && cookie.key?(:u) && cookie.key?(:u) && cookie.key?(:a)
26
+ end
27
+
28
+ def cookie_block_action
29
+ @decoded_cookie[:a]
30
+ end
31
+
32
+ def secured?
33
+ hmac_string = "#{@px_cookie}#{@px_ctx.context[:user_agent]}"
34
+ return hmac_valid?(hmac_string, cookie_hmac)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,148 @@
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 PerimeterxPayload
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[:cookie_origin] == 'header'
17
+ if (px_ctx.context[:px_cookie].key?(:v3))
18
+ return PerimeterxTokenV3.new(px_config,px_ctx)
19
+ end
20
+ return PerimeterxTokenV1.new(px_config,px_ctx)
21
+ elsif (px_ctx.context[:px_cookie].key?(:v3))
22
+ return PerimeterxCookieV3.new(px_config, px_ctx)
23
+ end
24
+ return PerimeterxCookieV1.new(px_config, px_ctx)
25
+ end
26
+
27
+ def cookie_score
28
+ #abstract, must be implemented
29
+ raise Exception.new("Unimplemented method")
30
+ end
31
+
32
+ def cookie_hmac
33
+ #abstract, must be implemented
34
+ raise Exception.new("Unimplemented method")
35
+ end
36
+
37
+ def valid_format?(cookie)
38
+ #abstract, must be implemented
39
+ raise Exception.new("Unimplemented method")
40
+ end
41
+
42
+ def cookie_block_action
43
+ #abstract, must be implemented
44
+ raise Exception.new("Unimplemented method")
45
+ end
46
+
47
+ def secured?
48
+ #abstract, must be implemented
49
+ raise Exception.new("Unimplemented method")
50
+ end
51
+
52
+ def is_valid?
53
+ return deserialize && !expired? && secured?
54
+ end
55
+
56
+ def cookie_time
57
+ return @decoded_cookie[:t]
58
+ end
59
+
60
+ def cookie_uuid
61
+ return @decoded_cookie[:u]
62
+ end
63
+
64
+ def cookie_vid
65
+ return @decoded_cookie[:v]
66
+ end
67
+
68
+ def high_score?
69
+ return cookie_score >= @px_config[:blocking_score]
70
+ end
71
+
72
+ def expired?
73
+ return cookie_time < (Time.now.to_f*1000).floor
74
+ end
75
+
76
+
77
+ def deserialize
78
+ if (!@decoded_cookie.nil?)
79
+ return true
80
+ end
81
+
82
+ # Decode or decrypt, depends on configuration
83
+ if (@px_config[:encryption_enabled])
84
+ cookie = decrypt(@px_cookie)
85
+ else
86
+ cookie = decode(@px_cookie)
87
+ end
88
+
89
+ if (cookie.nil?)
90
+ return false
91
+ end
92
+
93
+ if (!valid_format?(cookie))
94
+ return false
95
+ end
96
+
97
+ @decoded_cookie = cookie
98
+
99
+ return true
100
+ end
101
+
102
+
103
+ def decrypt(px_cookie)
104
+ begin
105
+ if (px_cookie.nil?)
106
+ return
107
+ end
108
+ px_cookie = px_cookie.gsub(' ', '+')
109
+ salt, iterations, cipher_text = px_cookie.split(':')
110
+ iterations = iterations.to_i
111
+ if (iterations > @px_config[:risk_cookie_max_iterations] || iterations < 500)
112
+ return
113
+ end
114
+ salt = Base64.decode64(salt)
115
+ cipher_text = Base64.decode64(cipher_text)
116
+ digest = OpenSSL::Digest::SHA256.new
117
+ value = OpenSSL::PKCS5.pbkdf2_hmac(@px_config[:cookie_key], salt, iterations, 48, digest)
118
+ key = value[0..31]
119
+ iv = value[32..-1]
120
+ cipher = OpenSSL::Cipher::AES256.new(:CBC)
121
+ cipher.decrypt
122
+ cipher.key = key
123
+ cipher.iv = iv
124
+ plaintext = cipher.update(cipher_text) + cipher.final
125
+
126
+ return eval(plaintext)
127
+ rescue Exception => e
128
+ @logger.debug("PerimeterxCookie[decrypt]: Cookie decrypt fail #{e.message}")
129
+ raise PxCookieDecryptionException.new("Cookie decrypt fail => #{e.message}");
130
+ end
131
+ end
132
+
133
+ def decode(px_cookie)
134
+ return eval(Base64.decode64(px_cookie))
135
+ end
136
+
137
+
138
+ def hmac_valid?(hmac_str, cookie_hmac)
139
+ hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, @cookie_secret, hmac_str)
140
+ # ref: https://thisdata.com/blog/timing-attacks-against-string-comparison/
141
+ password_correct = ActiveSupport::SecurityUtils.secure_compare(
142
+ ::Digest::SHA256.hexdigest(cookie_hmac),
143
+ ::Digest::SHA256.hexdigest(hmac)
144
+ )
145
+
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,38 @@
1
+ module PxModule
2
+ class PerimeterxTokenV1 < PerimeterxPayload
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('PerimeterxTokenV1[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
+ hmac_str = "#{cookie_time}#{@decoded_cookie[:s][:a]}#{cookie_score}#{cookie_uuid}#{cookie_vid}"
32
+
33
+ return hmac_valid?(hmac_str, cookie_hmac)
34
+ end
35
+
36
+ end
37
+
38
+ end