perimeter_x 1.0.6.pre.alpha → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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