perimeter_x 1.2.0 → 2.2.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 (34) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -0
  3. data/.travis.yml +3 -0
  4. data/Dockerfile +12 -7
  5. data/Gemfile.lock +36 -30
  6. data/Rakefile +1 -0
  7. data/changelog.md +58 -0
  8. data/examples/app/controllers/home_controller.rb +1 -1
  9. data/lib/perimeter_x.rb +195 -71
  10. data/lib/perimeterx/configuration.rb +74 -22
  11. data/lib/perimeterx/internal/clients/perimeter_x_activity_client.rb +32 -6
  12. data/lib/perimeterx/internal/exceptions/px_config_exception.rb +6 -0
  13. data/lib/perimeterx/internal/first_party/px_first_party.rb +124 -0
  14. data/lib/perimeterx/internal/{perimeter_x_cookie_v1.rb → payload/perimeter_x_cookie_v1.rb} +1 -1
  15. data/lib/perimeterx/internal/{perimeter_x_cookie_v3.rb → payload/perimeter_x_cookie_v3.rb} +1 -1
  16. data/lib/perimeterx/internal/{perimeter_x_cookie.rb → payload/perimeter_x_payload.rb} +12 -4
  17. data/lib/perimeterx/internal/payload/perimeter_x_token_v1.rb +38 -0
  18. data/lib/perimeterx/internal/payload/perimeter_x_token_v3.rb +36 -0
  19. data/lib/perimeterx/internal/perimeter_x_context.rb +74 -32
  20. data/lib/perimeterx/internal/validators/hash_schema_validator.rb +26 -0
  21. data/lib/perimeterx/internal/validators/perimeter_x_cookie_validator.rb +29 -21
  22. data/lib/perimeterx/internal/validators/perimeter_x_s2s_validator.rb +33 -9
  23. data/lib/perimeterx/utils/px_constants.rb +35 -17
  24. data/lib/perimeterx/utils/px_http_client.rb +60 -3
  25. data/lib/perimeterx/utils/px_template_factory.rb +18 -8
  26. data/lib/perimeterx/utils/templates/block_template.mustache +175 -0
  27. data/lib/perimeterx/utils/templates/ratelimit.mustache +9 -0
  28. data/lib/perimeterx/version.rb +1 -1
  29. data/perimeter_x.gemspec +3 -3
  30. data/readme.md +115 -31
  31. metadata +24 -20
  32. data/lib/perimeterx/internal/validators/perimeter_x_captcha_validator.rb +0 -65
  33. data/lib/perimeterx/utils/templates/block.mustache +0 -146
  34. data/lib/perimeterx/utils/templates/captcha.mustache +0 -185
@@ -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 => 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,
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: [FalseClass, TrueClass], 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,7 +17,12 @@ 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]
25
+
21
26
  px_data = {
22
27
  :type => activity_type,
23
28
  :headers => format_headers(px_ctx),
@@ -47,40 +52,61 @@ module PxModule
47
52
 
48
53
  def send_block_activity(px_ctx)
49
54
  @logger.debug("PerimeterxActivitiesClients[send_block_activity]")
50
- if (!@px_config[:send_page_acitivites])
55
+ if (!@px_config[:send_block_activities])
51
56
  @logger.debug("PerimeterxActivitiesClients[send_block_activity]: sending activites is disabled")
52
57
  return
53
58
  end
54
59
 
55
60
  details = {
56
- :block_uuid => px_ctx.context[:uuid],
57
- :block_score => px_ctx.context[:score],
58
- :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
59
67
  }
60
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
+
61
77
  send_to_perimeterx(PxModule::BLOCK_ACTIVITY, px_ctx, details)
62
78
 
63
79
  end
64
80
 
65
81
  def send_page_requested_activity(px_ctx)
66
82
  @logger.debug("PerimeterxActivitiesClients[send_page_requested_activity]")
67
- if (!@px_config[:send_page_acitivites])
83
+ if (!@px_config[:send_page_activities])
68
84
  return
69
85
  end
70
86
 
71
87
  details = {
72
88
  :http_version => px_ctx.context[:http_version],
73
- :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]
74
92
  }
75
93
 
76
94
  if (px_ctx.context.key?(:decoded_cookie))
77
95
  details[:px_cookie] = px_ctx.context[:decoded_cookie]
78
96
  end
79
97
 
98
+ if (px_ctx.context.key?(:px_orig_cookie))
99
+ details[:px_orig_cookie] = px_ctx.context[:px_orig_cookie]
100
+ end
101
+
80
102
  if (px_ctx.context.key?(:cookie_hmac))
81
103
  details[:px_cookie_hmac] = px_ctx.context[:cookie_hmac]
82
104
  end
83
105
 
106
+ if (px_ctx.context.key?(:risk_rtt))
107
+ details[:risk_rtt] = px_ctx.context[:risk_rtt]
108
+ end
109
+
84
110
  send_to_perimeterx(PxModule::PAGE_REQUESTED_ACTIVITY, px_ctx, details)
85
111
  end
86
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
@@ -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