perimeter_x 1.3.0 → 2.2.1

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.
@@ -1,37 +1,89 @@
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 => 1,
21
- :api_timeout => 1,
22
- :max_buffer_len => 10,
23
- :send_page_activities => true,
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,
34
+ :first_party_enabled => true
30
35
  }
31
36
 
37
+ CONFIG_SCHEMA = {
38
+ :app_id => {types: [String], required: true},
39
+ :cookie_key => {types: [String], required: true},
40
+ :auth_token => {types: [String], required: true},
41
+ :module_enabled => {types: [FalseClass, TrueClass], required: false},
42
+ :challenge_enabled => {types: [FalseClass, TrueClass], required: false},
43
+ :encryption_enabled => {types: [FalseClass, TrueClass], required: false},
44
+ :blocking_score => {types: [Integer], required: false},
45
+ :sensitive_headers => {types: [Array], allowed_element_types: [String], required: false},
46
+ :api_timeout_connection => {types: [Integer, Float], required: false},
47
+ :api_timeout => {types: [Integer, Float], required: false},
48
+ :send_page_activities => {types: [FalseClass, TrueClass], required: false},
49
+ :send_block_activities => {types: [FalseClass, TrueClass], required: false},
50
+ :sdk_name => {types: [String], required: false},
51
+ :debug => {types: [FalseClass, TrueClass], required: false},
52
+ :module_mode => {types: [Integer], required: false},
53
+ :sensitive_routes => {types: [Array], allowed_element_types: [String], required: false},
54
+ :whitelist_routes => {types: [Array], allowed_element_types: [String, Regexp], required: false},
55
+ :ip_headers => {types: [Array], allowed_element_types: [String], required: false},
56
+ :ip_header_function => {types: [Proc], required: false},
57
+ :bypass_monitor_header => {types: [String], required: false},
58
+ :risk_cookie_max_iterations => {types: [Integer], required: false},
59
+ :custom_verification_handler => {types: [Proc], required: false},
60
+ :additional_activity_handler => {types: [Proc], required: false},
61
+ :custom_logo => {types: [String], required: false},
62
+ :css_ref => {types: [String], required: false},
63
+ :js_ref => {types: [String], required: false},
64
+ :custom_uri => {types: [Proc], required: false},
65
+ :first_party_enabled => {types: [FalseClass, TrueClass], required: false}
66
+
67
+ }
68
+
69
+ def self.set_basic_config(basic_config)
70
+ if @@basic_config.nil?
71
+ @@mutex.synchronize {
72
+ @@basic_config = PX_DEFAULT.merge(basic_config)
73
+ }
74
+ end
75
+ end
76
+
32
77
  def initialize(params)
33
- PX_DEFAULT[:perimeterx_server_host] = "https://sapi-#{params[:app_id].downcase}.perimeterx.net"
34
- @configuration = PX_DEFAULT.merge(params)
78
+ if ! @@basic_config.is_a?(Hash)
79
+ raise PxConfigurationException.new('PerimeterX: Please initialize PerimeterX first')
80
+ end
81
+
82
+ # merge request configuration into the basic configuration
83
+ @configuration = @@basic_config.merge(params)
84
+ validate_hash_schema(@configuration, CONFIG_SCHEMA)
85
+
86
+ @configuration[:backend_url] = "https://sapi-#{@configuration[:app_id].downcase}.perimeterx.net"
35
87
  @configuration[:logger] = PxLogger.new(@configuration[:debug])
36
88
  end
37
89
  end
@@ -17,8 +17,11 @@ module PxModule
17
17
  @px_config[:additional_activity_handler].call(activity_type, px_ctx, details)
18
18
  end
19
19
 
20
+ if !px_ctx.context[:px_cookie].empty?
21
+ details[:cookie_origin] = px_ctx.context[:cookie_origin]
22
+ end
23
+
20
24
  details[:module_version] = @px_config[:sdk_name]
21
- details[:cookie_origin] = px_ctx.context[:cookie_origin]
22
25
 
23
26
  px_data = {
24
27
  :type => activity_type,
@@ -49,41 +52,61 @@ module PxModule
49
52
 
50
53
  def send_block_activity(px_ctx)
51
54
  @logger.debug("PerimeterxActivitiesClients[send_block_activity]")
52
- if (!@px_config[:send_page_acitivites])
55
+ if (!@px_config[:send_block_activities])
53
56
  @logger.debug("PerimeterxActivitiesClients[send_block_activity]: sending activites is disabled")
54
57
  return
55
58
  end
56
59
 
57
60
  details = {
58
- :block_uuid => px_ctx.context[:uuid],
61
+ :http_version => px_ctx.context[:http_version],
62
+ :http_method => px_ctx.context[:http_method],
63
+ :client_uuid => px_ctx.context[:uuid],
59
64
  :block_score => px_ctx.context[:score],
60
- :block_reason => px_ctx.context[:blocking_reason]
65
+ :block_reason => px_ctx.context[:blocking_reason],
66
+ :simulated_block => @px_config[:module_mode] == PxModule::MONITOR_MODE
61
67
  }
62
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
+
63
77
  send_to_perimeterx(PxModule::BLOCK_ACTIVITY, px_ctx, details)
64
78
 
65
79
  end
66
80
 
67
81
  def send_page_requested_activity(px_ctx)
68
82
  @logger.debug("PerimeterxActivitiesClients[send_page_requested_activity]")
69
- if (!@px_config[:send_page_acitivites])
83
+ if (!@px_config[:send_page_activities])
70
84
  return
71
85
  end
72
86
 
73
87
  details = {
74
88
  :http_version => px_ctx.context[:http_version],
75
89
  :http_method => px_ctx.context[:http_method],
76
- :client_uuid => px_ctx.context[:uuid]
90
+ :client_uuid => px_ctx.context[:uuid],
91
+ :pass_reason => px_ctx.context[:pass_reason]
77
92
  }
78
93
 
79
94
  if (px_ctx.context.key?(:decoded_cookie))
80
95
  details[:px_cookie] = px_ctx.context[:decoded_cookie]
81
96
  end
82
97
 
98
+ if (px_ctx.context.key?(:px_orig_cookie))
99
+ details[:px_orig_cookie] = px_ctx.context[:px_orig_cookie]
100
+ end
101
+
83
102
  if (px_ctx.context.key?(:cookie_hmac))
84
103
  details[:px_cookie_hmac] = px_ctx.context[:cookie_hmac]
85
104
  end
86
105
 
106
+ if (px_ctx.context.key?(:risk_rtt))
107
+ details[:risk_rtt] = px_ctx.context[:risk_rtt]
108
+ end
109
+
87
110
  send_to_perimeterx(PxModule::PAGE_REQUESTED_ACTIVITY, px_ctx, details)
88
111
  end
89
112
  end
@@ -0,0 +1,6 @@
1
+ class PxConfigurationException < StandardError
2
+ def initialize(msg)
3
+ super(msg)
4
+ end
5
+ end
6
+
@@ -0,0 +1,124 @@
1
+ require 'perimeterx/internal/perimeter_x_context'
2
+ module PxModule
3
+ class FirstPartyManager
4
+ def initialize(px_config, px_http_client, logger)
5
+ @px_config = px_config
6
+ @app_id = px_config[:app_id]
7
+ @px_http_client = px_http_client
8
+ @logger = logger
9
+ @from = [
10
+ "/#{@app_id[2..-1]}/init.js",
11
+ "/#{@app_id[2..-1]}/captcha",
12
+ "/#{@app_id[2..-1]}/xhr"
13
+ ]
14
+ end
15
+
16
+ def send_first_party_request(req)
17
+ uri = URI.parse(req.original_url)
18
+ url_path = uri.path
19
+
20
+ headers = extract_headers(req)
21
+ headers["x-px-first-party"] = "1"
22
+ headers["x-px-enforcer-true-ip"] = PerimeterXContext.extract_ip(req, @px_config)
23
+
24
+ if url_path.start_with?(@from[0])
25
+ return get_client(req, uri, headers)
26
+ elsif url_path.start_with?(@from[1])
27
+ return get_captcha(req, uri, headers)
28
+ elsif url_path.start_with?(@from[2])
29
+ return send_xhr(req, uri, headers)
30
+ else
31
+ return nil
32
+ end
33
+ end
34
+
35
+ def get_client(req, uri, headers)
36
+ @logger.debug("FirstPartyManager[get_client]")
37
+
38
+ # define host
39
+ headers["host"] = PxModule::CLIENT_HOST
40
+
41
+ # define request url
42
+ url = "#{uri.scheme}://#{PxModule::CLIENT_HOST}/#{@app_id}/main.min.js"
43
+
44
+ # send request
45
+ return @px_http_client.get(url, headers)
46
+ end
47
+
48
+ def get_captcha(req, uri, headers)
49
+ @logger.debug("FirstPartyManager[get_captcha]")
50
+
51
+ # define host
52
+ headers["host"] = PxModule::CAPTCHA_HOST
53
+
54
+ # define request url
55
+ path_and_query = uri.request_uri
56
+ uri_suffix = path_and_query.sub "/#{@app_id[2..-1]}/captcha", ""
57
+ url = "#{uri.scheme}://#{PxModule::CAPTCHA_HOST}#{uri_suffix}"
58
+
59
+ # send request
60
+ return @px_http_client.get(url, headers)
61
+ end
62
+
63
+ def send_xhr(req, uri, headers)
64
+ @logger.debug("FirstPartyManager[send_xhr]")
65
+
66
+ # handle vid cookies
67
+ if !req.cookies.nil?
68
+ if req.cookies.key?("_pxvid")
69
+ vid = PerimeterXContext.force_utf8(req.cookies["_pxvid"])
70
+ if headers.key?('cookie')
71
+ headers['cookie'] += "; pxvid=#{vid}";
72
+ else
73
+ headers['cookie'] = "pxvid=#{vid}";
74
+ end
75
+ end
76
+ end
77
+
78
+ # define host
79
+ headers["host"] = "collector-#{@app_id.downcase}.perimeterx.net"
80
+
81
+ # define request url
82
+ path_and_query = uri.request_uri
83
+ path_suffix = path_and_query.sub "/#{@app_id[2..-1]}/xhr", ""
84
+ url = "#{uri.scheme}://collector-#{@app_id.downcase}.perimeterx.net#{path_suffix}"
85
+
86
+ # send request
87
+ return @px_http_client.post_xhr(url, req.body.string, headers)
88
+ end
89
+
90
+ def extract_headers(req)
91
+ headers = Hash.new
92
+ req.headers.each do |k, v|
93
+ if (k.start_with? 'HTTP_') && (!@px_config[:sensitive_headers].include? k)
94
+ header = k.to_s.gsub('HTTP_', '')
95
+ header = header.gsub('_', '-').downcase
96
+ headers[header] = PerimeterXContext.force_utf8(v)
97
+ end
98
+ end
99
+ return headers
100
+ end
101
+
102
+ # -1 - not first party request
103
+ # 0 - /init.js
104
+ # 1 - /captcha
105
+ # 2 - /xhr
106
+ def get_first_party_request_type(req)
107
+ url_path = URI.parse(req.original_url).path
108
+ @from.each_with_index do |val,index|
109
+ if url_path.start_with?(val)
110
+ return index
111
+ end
112
+ end
113
+ return -1
114
+ end
115
+
116
+ def is_first_party_request(req)
117
+ return get_first_party_request_type(req) != -1
118
+ end
119
+
120
+ def get_response_content_type(req)
121
+ return get_first_party_request_type(req) == 2 ? :json : :js
122
+ end
123
+ end
124
+ end
@@ -108,6 +108,9 @@ module PxModule
108
108
  px_cookie = px_cookie.gsub(' ', '+')
109
109
  salt, iterations, cipher_text = px_cookie.split(':')
110
110
  iterations = iterations.to_i
111
+ if (iterations > @px_config[:risk_cookie_max_iterations] || iterations < 500)
112
+ return
113
+ end
111
114
  salt = Base64.decode64(salt)
112
115
  cipher_text = Base64.decode64(cipher_text)
113
116
  digest = OpenSSL::Digest::SHA256.new
@@ -1,10 +1,33 @@
1
1
  require 'perimeterx/utils/px_logger'
2
+ require 'perimeterx/utils/px_constants'
2
3
 
3
4
  module PxModule
4
5
  class PerimeterXContext
5
6
 
6
7
  attr_accessor :context
7
8
  attr_accessor :px_config
9
+
10
+ # class methods
11
+
12
+ def self.extract_ip(req, px_config)
13
+ # Get IP from header/custom function
14
+ if px_config[:ip_headers].length() > 0
15
+ px_config[:ip_headers].each do |ip_header|
16
+ if req.headers[ip_header]
17
+ return PerimeterXContext.force_utf8(req.headers[ip_header])
18
+ end
19
+ end
20
+ elsif px_config[:ip_header_function] != nil
21
+ return px_config[:ip_header_function].call(req)
22
+ end
23
+ return req.ip
24
+ end
25
+
26
+ def self.force_utf8(str)
27
+ return str.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '')
28
+ end
29
+
30
+ # instance methods
8
31
 
9
32
  def initialize(px_config, req)
10
33
  @logger = px_config[:logger]
@@ -14,27 +37,41 @@ module PxModule
14
37
  @context[:px_cookie] = Hash.new
15
38
  @context[:headers] = Hash.new
16
39
  @context[:cookie_origin] = 'cookie'
40
+ @context[:made_s2s_risk_api_call] = false
41
+ @context[:first_party_enabled] = px_config[:first_party_enabled]
42
+
17
43
  cookies = req.cookies
18
44
 
45
+ @context[:ip] = PerimeterXContext.extract_ip(req, px_config)
46
+
19
47
  # Get token from header
20
48
  if req.headers[PxModule::TOKEN_HEADER]
21
49
  @context[:cookie_origin] = 'header'
22
- token = req.headers[PxModule::TOKEN_HEADER]
23
- if token.include? ':'
24
- exploded_token = token.split(':', 2)
25
- cookie_sym = "v#{exploded_token[0]}".to_sym
26
- @context[:px_cookie][cookie_sym] = exploded_token[1]
50
+ token = PerimeterXContext.force_utf8(req.headers[PxModule::TOKEN_HEADER])
51
+ if token.match(PxModule::MOBILE_TOKEN_V3_REGEX)
52
+ @context[:px_cookie][:v3] = token[2..-1]
53
+ elsif token.match(PxModule::MOBILE_ERROR_REGEX)
54
+ @context[:mobile_error] = token
55
+ if req.headers[PxModule::ORIGINAL_TOKEN_HEADER]
56
+ token = PerimeterXContext.force_utf8(req.headers[PxModule::ORIGINAL_TOKEN_HEADER])
57
+ if token.match(PxModule::MOBILE_TOKEN_V3_REGEX)
58
+ @context[:px_cookie][:v3] = token[2..-1]
59
+ end
60
+ end
27
61
  end
28
62
  elsif !cookies.empty? # Get cookie from jar
29
63
  # Prepare hashed cookies
30
64
  cookies.each do |k, v|
31
65
  case k.to_s
32
66
  when '_px3'
33
- @context[:px_cookie][:v3] = v
67
+ @context[:px_cookie][:v3] = PerimeterXContext.force_utf8(v)
34
68
  when '_px'
35
- @context[:px_cookie][:v1] = v
36
- when '_pxCaptcha'
37
- @context[:px_captcha] = v
69
+ @context[:px_cookie][:v1] = PerimeterXContext.force_utf8(v)
70
+ when '_pxvid'
71
+ if v.is_a?(String) && v.match(PxModule::VID_REGEX)
72
+ @context[:vid_source] = "vid_cookie"
73
+ @context[:vid] = PerimeterXContext.force_utf8(v)
74
+ end
38
75
  end
39
76
  end #end case
40
77
  end #end empty cookies
@@ -43,10 +80,10 @@ module PxModule
43
80
  if (k.start_with? 'HTTP_')
44
81
  header = k.to_s.gsub('HTTP_', '')
45
82
  header = header.gsub('_', '-').downcase
46
- @context[:headers][header.to_sym] = v
83
+ @context[:headers][header.to_sym] = PerimeterXContext.force_utf8(v)
47
84
  end
48
85
  end #end headers foreach
49
-
86
+
50
87
  @context[:hostname]= req.server_name
51
88
  @context[:user_agent] = req.user_agent ? req.user_agent : ''
52
89
  @context[:uri] = px_config[:custom_uri] ? px_config[:custom_uri].call(req) : req.fullpath
@@ -54,14 +91,6 @@ module PxModule
54
91
  @context[:format] = req.format.symbol
55
92
  @context[:score] = 0
56
93
 
57
- if px_config.key?(:custom_user_ip)
58
- @context[:ip] = req.headers[px_config[:custom_user_ip]]
59
- elsif px_config.key?(:px_custom_user_ip_method)
60
- @context[:ip] = px_config[:px_custom_user_ip_method].call(req)
61
- else
62
- @context[:ip] = req.ip
63
- end
64
-
65
94
  if req.server_protocol
66
95
  httpVer = req.server_protocol.split('/')
67
96
  if httpVer.size > 0
@@ -87,6 +116,8 @@ module PxModule
87
116
  return 'block'
88
117
  when 'j'
89
118
  return 'challenge'
119
+ when 'r'
120
+ return 'rate_limit'
90
121
  else
91
122
  return captcha
92
123
  end