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.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/.travis.yml +3 -0
- data/Dockerfile +12 -7
- data/Gemfile.lock +36 -30
- data/Rakefile +1 -0
- data/changelog.md +58 -0
- data/examples/app/controllers/home_controller.rb +1 -1
- data/lib/perimeter_x.rb +195 -71
- data/lib/perimeterx/configuration.rb +74 -22
- data/lib/perimeterx/internal/clients/perimeter_x_activity_client.rb +32 -6
- data/lib/perimeterx/internal/exceptions/px_config_exception.rb +6 -0
- data/lib/perimeterx/internal/first_party/px_first_party.rb +124 -0
- data/lib/perimeterx/internal/{perimeter_x_cookie_v1.rb → payload/perimeter_x_cookie_v1.rb} +1 -1
- data/lib/perimeterx/internal/{perimeter_x_cookie_v3.rb → payload/perimeter_x_cookie_v3.rb} +1 -1
- data/lib/perimeterx/internal/{perimeter_x_cookie.rb → payload/perimeter_x_payload.rb} +12 -4
- data/lib/perimeterx/internal/payload/perimeter_x_token_v1.rb +38 -0
- data/lib/perimeterx/internal/payload/perimeter_x_token_v3.rb +36 -0
- data/lib/perimeterx/internal/perimeter_x_context.rb +74 -32
- data/lib/perimeterx/internal/validators/hash_schema_validator.rb +26 -0
- data/lib/perimeterx/internal/validators/perimeter_x_cookie_validator.rb +29 -21
- data/lib/perimeterx/internal/validators/perimeter_x_s2s_validator.rb +33 -9
- data/lib/perimeterx/utils/px_constants.rb +35 -17
- data/lib/perimeterx/utils/px_http_client.rb +60 -3
- data/lib/perimeterx/utils/px_template_factory.rb +18 -8
- data/lib/perimeterx/utils/templates/block_template.mustache +175 -0
- data/lib/perimeterx/utils/templates/ratelimit.mustache +9 -0
- data/lib/perimeterx/version.rb +1 -1
- data/perimeter_x.gemspec +3 -3
- data/readme.md +115 -31
- metadata +24 -20
- data/lib/perimeterx/internal/validators/perimeter_x_captcha_validator.rb +0 -65
- data/lib/perimeterx/utils/templates/block.mustache +0 -146
- 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
|
12
|
-
:cookie_key
|
13
|
-
:auth_token
|
14
|
-
:module_enabled
|
15
|
-
:
|
16
|
-
:
|
17
|
-
:
|
18
|
-
:
|
19
|
-
:
|
20
|
-
:
|
21
|
-
:
|
22
|
-
:
|
23
|
-
:
|
24
|
-
:
|
25
|
-
:
|
26
|
-
:
|
27
|
-
:
|
28
|
-
:
|
29
|
-
:
|
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
|
-
|
34
|
-
|
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[:
|
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
|
-
:
|
57
|
-
:
|
58
|
-
:
|
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[:
|
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,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
|
@@ -4,7 +4,7 @@ require 'openssl'
|
|
4
4
|
require 'perimeterx/internal/exceptions/px_cookie_decryption_exception'
|
5
5
|
|
6
6
|
module PxModule
|
7
|
-
class
|
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
|
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
|
-
|
135
|
-
|
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
|