perimeter_x 1.1.0 → 2.1.0

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