perimeter_x 1.1.0 → 2.1.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 (33) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -0
  3. data/.travis.yml +3 -0
  4. data/Dockerfile +19 -43
  5. data/Gemfile.lock +36 -30
  6. data/Rakefile +1 -0
  7. data/changelog.md +63 -0
  8. data/examples/app/controllers/home_controller.rb +1 -1
  9. data/lib/perimeter_x.rb +152 -70
  10. data/lib/perimeterx/configuration.rb +71 -22
  11. data/lib/perimeterx/internal/clients/perimeter_x_activity_client.rb +36 -15
  12. data/lib/perimeterx/internal/exceptions/px_config_exception.rb +6 -0
  13. data/lib/perimeterx/internal/{perimeter_x_cookie_v1.rb → payload/perimeter_x_cookie_v1.rb} +1 -1
  14. data/lib/perimeterx/internal/{perimeter_x_cookie_v3.rb → payload/perimeter_x_cookie_v3.rb} +1 -1
  15. data/lib/perimeterx/internal/{perimeter_x_cookie.rb → payload/perimeter_x_payload.rb} +12 -4
  16. data/lib/perimeterx/internal/payload/perimeter_x_token_v1.rb +38 -0
  17. data/lib/perimeterx/internal/payload/perimeter_x_token_v3.rb +36 -0
  18. data/lib/perimeterx/internal/perimeter_x_context.rb +66 -31
  19. data/lib/perimeterx/internal/validators/hash_schema_validator.rb +26 -0
  20. data/lib/perimeterx/internal/validators/perimeter_x_cookie_validator.rb +29 -21
  21. data/lib/perimeterx/internal/validators/perimeter_x_s2s_validator.rb +34 -10
  22. data/lib/perimeterx/utils/px_constants.rb +35 -17
  23. data/lib/perimeterx/utils/px_http_client.rb +29 -35
  24. data/lib/perimeterx/utils/px_template_factory.rb +18 -8
  25. data/lib/perimeterx/utils/templates/block_template.mustache +175 -0
  26. data/lib/perimeterx/utils/templates/ratelimit.mustache +9 -0
  27. data/lib/perimeterx/version.rb +1 -1
  28. data/perimeter_x.gemspec +5 -4
  29. data/readme.md +95 -32
  30. metadata +53 -24
  31. data/lib/perimeterx/internal/validators/perimeter_x_captcha_validator.rb +0 -65
  32. data/lib/perimeterx/utils/templates/block.mustache +0 -146
  33. data/lib/perimeterx/utils/templates/captcha.mustache +0 -185
@@ -1,37 +1,86 @@
1
1
  require 'perimeterx/utils/px_logger'
2
2
  require 'perimeterx/utils/px_constants'
3
+ require 'perimeterx/internal/validators/hash_schema_validator'
3
4
 
4
5
  module PxModule
5
6
  class Configuration
7
+ @@basic_config = nil
8
+ @@mutex = Mutex.new
6
9
 
7
10
  attr_accessor :configuration
8
- attr_accessor :PX_DEFAULT
9
11
 
10
12
  PX_DEFAULT = {
11
- :app_id => nil,
12
- :cookie_key => nil,
13
- :auth_token => nil,
14
- :module_enabled => true,
15
- :captcha_enabled => true,
16
- :challenge_enabled => true,
17
- :encryption_enabled => true,
18
- :blocking_score => 70,
19
- :sensitive_headers => ["http-cookie", "http-cookies"],
20
- :api_connect_timeout => 0,
21
- :api_timeout => 0,
22
- :max_buffer_len => 30,
23
- :send_page_activities => false,
24
- :send_block_activities => true,
25
- :sdk_name => PxModule::SDK_NAME,
26
- :debug => false,
27
- :module_mode => PxModule::ACTIVE_MODE,
28
- :local_proxy => false,
29
- :sensitive_routes => []
13
+ :app_id => nil,
14
+ :cookie_key => nil,
15
+ :auth_token => nil,
16
+ :module_enabled => true,
17
+ :challenge_enabled => true,
18
+ :encryption_enabled => true,
19
+ :blocking_score => 100,
20
+ :sensitive_headers => ["http-cookie", "http-cookies"],
21
+ :api_timeout_connection => 1,
22
+ :api_timeout => 1,
23
+ :send_page_activities => true,
24
+ :send_block_activities => true,
25
+ :sdk_name => PxModule::SDK_NAME,
26
+ :debug => false,
27
+ :module_mode => PxModule::MONITOR_MODE,
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
30
34
  }
31
35
 
36
+ CONFIG_SCHEMA = {
37
+ :app_id => {types: [String], required: true},
38
+ :cookie_key => {types: [String], required: true},
39
+ :auth_token => {types: [String], required: true},
40
+ :module_enabled => {types: [FalseClass, TrueClass], required: false},
41
+ :challenge_enabled => {types: [FalseClass, TrueClass], required: false},
42
+ :encryption_enabled => {types: [FalseClass, TrueClass], required: false},
43
+ :blocking_score => {types: [Integer], required: false},
44
+ :sensitive_headers => {types: [Array], allowed_element_types: [String], required: false},
45
+ :api_timeout_connection => {types: [Integer, Float], required: false},
46
+ :api_timeout => {types: [Integer, Float], required: false},
47
+ :send_page_activities => {types: [FalseClass, TrueClass], required: false},
48
+ :send_block_activities => {types: [FalseClass, TrueClass], required: false},
49
+ :sdk_name => {types: [String], required: false},
50
+ :debug => {types: [FalseClass, TrueClass], required: false},
51
+ :module_mode => {types: [Integer], required: false},
52
+ :sensitive_routes => {types: [Array], allowed_element_types: [String], required: false},
53
+ :whitelist_routes => {types: [Array], allowed_element_types: [String, Regexp], required: false},
54
+ :ip_headers => {types: [Array], allowed_element_types: [String], required: false},
55
+ :ip_header_function => {types: [Proc], required: false},
56
+ :bypass_monitor_header => {types: [FalseClass, TrueClass], required: false},
57
+ :risk_cookie_max_iterations => {types: [Integer], required: false},
58
+ :custom_verification_handler => {types: [Proc], required: false},
59
+ :additional_activity_handler => {types: [Proc], required: false},
60
+ :custom_logo => {types: [String], required: false},
61
+ :css_ref => {types: [String], required: false},
62
+ :js_ref => {types: [String], required: false},
63
+ :custom_uri => {types: [Proc], required: false}
64
+ }
65
+
66
+ def self.set_basic_config(basic_config)
67
+ if @@basic_config.nil?
68
+ @@mutex.synchronize {
69
+ @@basic_config = PX_DEFAULT.merge(basic_config)
70
+ }
71
+ end
72
+ end
73
+
32
74
  def initialize(params)
33
- PX_DEFAULT[:perimeterx_server_host] = "https://sapi-#{params[:app_id].downcase}.perimeterx.net"
34
- @configuration = PX_DEFAULT.merge(params);
75
+ if ! @@basic_config.is_a?(Hash)
76
+ raise PxConfigurationException.new('PerimeterX: Please initialize PerimeterX first')
77
+ end
78
+
79
+ # merge request configuration into the basic configuration
80
+ @configuration = @@basic_config.merge(params)
81
+ validate_hash_schema(@configuration, CONFIG_SCHEMA)
82
+
83
+ @configuration[:backend_url] = "https://sapi-#{@configuration[:app_id].downcase}.perimeterx.net"
35
84
  @configuration[:logger] = PxLogger.new(@configuration[:debug])
36
85
  end
37
86
  end
@@ -3,12 +3,10 @@ require 'perimeterx/internal/clients/perimeter_x_risk_client'
3
3
  module PxModule
4
4
  class PerimeterxActivitiesClient < PerimeterxRiskClient
5
5
 
6
- attr_accessor :activities
7
6
 
8
7
  def initialize(px_config, http_client)
9
8
  super(px_config, http_client)
10
9
  @logger.debug("PerimeterxActivitiesClients[initialize]")
11
- @activities = [];
12
10
  end
13
11
 
14
12
  def send_to_perimeterx(activity_type, px_ctx, details = [])
@@ -19,7 +17,12 @@ module PxModule
19
17
  @px_config[:additional_activity_handler].call(activity_type, px_ctx, details)
20
18
  end
21
19
 
20
+ if !px_ctx.context[:px_cookie].empty?
21
+ details[:cookie_origin] = px_ctx.context[:cookie_origin]
22
+ end
23
+
22
24
  details[:module_version] = @px_config[:sdk_name]
25
+
23
26
  px_data = {
24
27
  :type => activity_type,
25
28
  :headers => format_headers(px_ctx),
@@ -41,51 +44,69 @@ module PxModule
41
44
  "Content-Type" => "application/json"
42
45
  };
43
46
 
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
47
+ s = Time.now
48
+ @http_client.async.post(PxModule::API_V1_S2S, px_data, headers)
49
+ e = Time.now
50
+ @logger.debug("PerimeterxActivitiesClients[send_to_perimeterx]: post runtime #{(e-s)*1000}")
51
51
  end
52
52
 
53
53
  def send_block_activity(px_ctx)
54
54
  @logger.debug("PerimeterxActivitiesClients[send_block_activity]")
55
- if (!@px_config[:send_page_acitivites])
55
+ if (!@px_config[:send_block_activities])
56
56
  @logger.debug("PerimeterxActivitiesClients[send_block_activity]: sending activites is disabled")
57
57
  return
58
58
  end
59
59
 
60
60
  details = {
61
- :block_uuid => px_ctx.context[:uuid],
62
- :block_score => px_ctx.context[:score],
63
- :block_reason => px_ctx.context[:block_reason]
61
+ :http_version => px_ctx.context[:http_version],
62
+ :http_method => px_ctx.context[:http_method],
63
+ :client_uuid => px_ctx.context[:uuid],
64
+ :block_score => px_ctx.context[:score],
65
+ :block_reason => px_ctx.context[:blocking_reason],
66
+ :simulated_block => @px_config[:module_mode] == PxModule::MONITOR_MODE
64
67
  }
65
68
 
69
+ if (px_ctx.context.key?(:risk_rtt))
70
+ details[:risk_rtt] = px_ctx.context[:risk_rtt]
71
+ end
72
+
73
+ if (px_ctx.context.key?(:px_orig_cookie))
74
+ details[:px_orig_cookie] = px_ctx.context[:px_orig_cookie]
75
+ end
76
+
66
77
  send_to_perimeterx(PxModule::BLOCK_ACTIVITY, px_ctx, details)
67
78
 
68
79
  end
69
80
 
70
81
  def send_page_requested_activity(px_ctx)
71
82
  @logger.debug("PerimeterxActivitiesClients[send_page_requested_activity]")
72
- if (!@px_config[:send_page_acitivites])
83
+ if (!@px_config[:send_page_activities])
73
84
  return
74
85
  end
75
86
 
76
87
  details = {
77
88
  :http_version => px_ctx.context[:http_version],
78
- :http_method => px_ctx.context[:http_method]
89
+ :http_method => px_ctx.context[:http_method],
90
+ :client_uuid => px_ctx.context[:uuid],
91
+ :pass_reason => px_ctx.context[:pass_reason]
79
92
  }
80
93
 
81
94
  if (px_ctx.context.key?(:decoded_cookie))
82
95
  details[:px_cookie] = px_ctx.context[:decoded_cookie]
83
96
  end
84
97
 
98
+ if (px_ctx.context.key?(:px_orig_cookie))
99
+ details[:px_orig_cookie] = px_ctx.context[:px_orig_cookie]
100
+ end
101
+
85
102
  if (px_ctx.context.key?(:cookie_hmac))
86
103
  details[:px_cookie_hmac] = px_ctx.context[:cookie_hmac]
87
104
  end
88
105
 
106
+ if (px_ctx.context.key?(:risk_rtt))
107
+ details[:risk_rtt] = px_ctx.context[:risk_rtt]
108
+ end
109
+
89
110
  send_to_perimeterx(PxModule::PAGE_REQUESTED_ACTIVITY, px_ctx, details)
90
111
  end
91
112
  end
@@ -0,0 +1,6 @@
1
+ class PxConfigurationException < StandardError
2
+ def initialize(msg)
3
+ super(msg)
4
+ end
5
+ end
6
+
@@ -1,5 +1,5 @@
1
1
  module PxModule
2
- class PerimeterxCookieV1 < PerimeterxCookie
2
+ class PerimeterxCookieV1 < PerimeterxPayload
3
3
 
4
4
  attr_accessor :px_config, :px_ctx
5
5
 
@@ -1,5 +1,5 @@
1
1
  module PxModule
2
- class PerimeterxCookieV3 < PerimeterxCookie
2
+ class PerimeterxCookieV3 < PerimeterxPayload
3
3
 
4
4
  attr_accessor :px_config, :px_ctx, :cookie_hash
5
5
 
@@ -4,7 +4,7 @@ require 'openssl'
4
4
  require 'perimeterx/internal/exceptions/px_cookie_decryption_exception'
5
5
 
6
6
  module PxModule
7
- class PerimeterxCookie
7
+ class PerimeterxPayload
8
8
  attr_accessor :px_cookie, :px_config, :px_ctx, :cookie_secret, :decoded_cookie
9
9
 
10
10
  def initialize(px_config)
@@ -13,7 +13,12 @@ module PxModule
13
13
  end
14
14
 
15
15
  def self.px_cookie_factory(px_ctx, px_config)
16
- if (px_ctx.context[:px_cookie].key?(:v3))
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))
17
22
  return PerimeterxCookieV3.new(px_config, px_ctx)
18
23
  end
19
24
  return PerimeterxCookieV1.new(px_config, px_ctx)
@@ -103,6 +108,9 @@ module PxModule
103
108
  px_cookie = px_cookie.gsub(' ', '+')
104
109
  salt, iterations, cipher_text = px_cookie.split(':')
105
110
  iterations = iterations.to_i
111
+ if (iterations > @px_config[:risk_cookie_max_iterations] || iterations < 500)
112
+ return
113
+ end
106
114
  salt = Base64.decode64(salt)
107
115
  cipher_text = Base64.decode64(cipher_text)
108
116
  digest = OpenSSL::Digest::SHA256.new
@@ -131,8 +139,8 @@ module PxModule
131
139
  hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, @cookie_secret, hmac_str)
132
140
  # ref: https://thisdata.com/blog/timing-attacks-against-string-comparison/
133
141
  password_correct = ActiveSupport::SecurityUtils.secure_compare(
134
- ::Digest::SHA256.hexdigest(cookie_hmac),
135
- ::Digest::SHA256.hexdigest(hmac)
142
+ ::Digest::SHA256.hexdigest(cookie_hmac),
143
+ ::Digest::SHA256.hexdigest(hmac)
136
144
  )
137
145
 
138
146
  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
@@ -0,0 +1,36 @@
1
+ module PxModule
2
+ class PerimeterxTokenV3 < 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('PerimeterxTokenV3[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
+ return hmac_valid?(@px_cookie, cookie_hmac)
34
+ end
35
+ end
36
+ end
@@ -1,4 +1,5 @@
1
1
  require 'perimeterx/utils/px_logger'
2
+ require 'perimeterx/utils/px_constants'
2
3
 
3
4
  module PxModule
4
5
  class PerimeterXContext
@@ -7,32 +8,68 @@ module PxModule
7
8
  attr_accessor :px_config
8
9
 
9
10
  def initialize(px_config, req)
10
- @logger = px_config[:logger];
11
- @logger.debug("PerimeterXContext[initialize] ")
11
+ @logger = px_config[:logger]
12
+ @logger.debug('PerimeterXContext[initialize]')
12
13
  @context = Hash.new
13
14
 
14
15
  @context[:px_cookie] = Hash.new
15
16
  @context[:headers] = Hash.new
17
+ @context[:cookie_origin] = 'cookie'
18
+ @context[:made_s2s_risk_api_call] = false
16
19
  cookies = req.cookies
17
- if (!cookies.empty?)
20
+
21
+ # Get IP from header/custom function
22
+ if px_config[:ip_headers].length() > 0
23
+ px_config[:ip_headers].each do |ip_header|
24
+ if req.headers[ip_header]
25
+ @context[:ip] = force_utf8(req.headers[ip_header])
26
+ end
27
+ end
28
+ elsif px_config[:ip_header_function] != nil
29
+ @context[:ip] = px_config[:ip_header_function].call(req)
30
+ end
31
+
32
+ if @context[:ip] == nil
33
+ @context[:ip] = req.ip
34
+ end
35
+
36
+ # Get token from header
37
+ if req.headers[PxModule::TOKEN_HEADER]
38
+ @context[:cookie_origin] = 'header'
39
+ token = force_utf8(req.headers[PxModule::TOKEN_HEADER])
40
+ if token.match(PxModule::MOBILE_TOKEN_V3_REGEX)
41
+ @context[:px_cookie][:v3] = token[2..-1]
42
+ elsif token.match(PxModule::MOBILE_ERROR_REGEX)
43
+ @context[:mobile_error] = token
44
+ if req.headers[PxModule::ORIGINAL_TOKEN_HEADER]
45
+ token = force_utf8(req.headers[PxModule::ORIGINAL_TOKEN_HEADER])
46
+ if token.match(PxModule::MOBILE_TOKEN_V3_REGEX)
47
+ @context[:px_cookie][:v3] = token[2..-1]
48
+ end
49
+ end
50
+ end
51
+ elsif !cookies.empty? # Get cookie from jar
18
52
  # Prepare hashed cookies
19
53
  cookies.each do |k, v|
20
54
  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
55
+ when '_px3'
56
+ @context[:px_cookie][:v3] = force_utf8(v)
57
+ when '_px'
58
+ @context[:px_cookie][:v1] = force_utf8(v)
59
+ when '_pxvid'
60
+ if v.is_a?(String) && v.match(PxModule::VID_REGEX)
61
+ @context[:vid_source] = "vid_cookie"
62
+ @context[:vid] = force_utf8(v)
63
+ end
27
64
  end
28
65
  end #end case
29
66
  end #end empty cookies
30
67
 
31
68
  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
69
+ if (k.start_with? 'HTTP_')
70
+ header = k.to_s.gsub('HTTP_', '')
71
+ header = header.gsub('_', '-').downcase
72
+ @context[:headers][header.to_sym] = force_utf8(v)
36
73
  end
37
74
  end #end headers foreach
38
75
 
@@ -43,18 +80,10 @@ module PxModule
43
80
  @context[:format] = req.format.symbol
44
81
  @context[:score] = 0
45
82
 
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
52
- end
53
-
54
83
  if req.server_protocol
55
- httpVer = req.server_protocol.split("/")
84
+ httpVer = req.server_protocol.split('/')
56
85
  if httpVer.size > 0
57
- @context[:http_version] = httpVer[1];
86
+ @context[:http_version] = httpVer[1]
58
87
  end
59
88
  end
60
89
  @context[:http_method] = req.method
@@ -65,19 +94,25 @@ module PxModule
65
94
  sensitive_routes.each do |sensitive_route|
66
95
  return true if uri.start_with? sensitive_route
67
96
  end
68
- return false
97
+ false
69
98
  end
70
99
 
100
+ def force_utf8(str)
101
+ return str.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '')
102
+ end
103
+
71
104
  def set_block_action_type(action)
72
105
  @context[:block_action] = case action
73
- when "c"
74
- "captcha"
75
- when "b"
76
- return "block"
77
- when "j"
78
- return "challenge"
106
+ when 'c'
107
+ 'captcha'
108
+ when 'b'
109
+ return 'block'
110
+ when 'j'
111
+ return 'challenge'
112
+ when 'r'
113
+ return 'rate_limit'
79
114
  else
80
- return "captcha"
115
+ return captcha
81
116
  end
82
117
  end
83
118