perimeter_x 1.2.0 → 2.2.0

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