perimeter_x 1.0.6.pre.alpha → 2.0.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 +6 -3
- data/.travis.yml +3 -0
- data/Dockerfile +19 -41
- data/Gemfile +1 -1
- data/Gemfile.lock +51 -3
- data/LICENSE.txt +9 -12
- data/Rakefile +10 -2
- data/changelog.md +72 -0
- data/examples/app/controllers/home_controller.rb +9 -0
- data/examples/app/views/home/index.html.erb.dist +20 -0
- data/examples/config/initializers/perimeterx.rb.dist +8 -0
- data/examples/{routes.rb → config/routes.rb} +0 -0
- data/lib/perimeter_x.rb +192 -37
- data/lib/perimeterx/configuration.rb +30 -18
- data/lib/perimeterx/internal/clients/perimeter_x_activity_client.rb +110 -0
- data/lib/perimeterx/internal/clients/perimeter_x_risk_client.rb +28 -0
- data/lib/perimeterx/internal/exceptions/px_cookie_decryption_exception.rb +5 -0
- data/lib/perimeterx/internal/payload/perimeter_x_cookie_v1.rb +42 -0
- data/lib/perimeterx/internal/payload/perimeter_x_cookie_v3.rb +37 -0
- data/lib/perimeterx/internal/payload/perimeter_x_payload.rb +148 -0
- 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 +112 -53
- data/lib/perimeterx/internal/validators/perimeter_x_cookie_validator.rb +103 -0
- data/lib/perimeterx/internal/validators/perimeter_x_s2s_validator.rb +128 -0
- data/lib/perimeterx/utils/px_constants.rb +62 -0
- data/lib/perimeterx/utils/px_http_client.rb +43 -28
- data/lib/perimeterx/utils/px_logger.rb +12 -6
- data/lib/perimeterx/utils/px_template_factory.rb +41 -0
- data/lib/perimeterx/utils/templates/block_template.mustache +175 -0
- data/lib/perimeterx/utils/templates/ratelimit.mustache +9 -0
- data/lib/perimeterx/version.rb +2 -2
- data/perimeter_x.gemspec +10 -4
- data/readme.md +258 -42
- metadata +130 -24
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/examples/home_controller.rb.dist +0 -23
- data/lib/perimeterx/internal/perimeter_x_risk_client.rb +0 -29
- data/lib/perimeterx/internal/perimeter_x_s2s_validator.rb +0 -67
@@ -1,30 +1,42 @@
|
|
1
|
-
|
1
|
+
require 'perimeterx/utils/px_logger'
|
2
|
+
require 'perimeterx/utils/px_constants'
|
3
|
+
|
4
|
+
module PxModule
|
2
5
|
class Configuration
|
3
6
|
|
4
7
|
attr_accessor :configuration
|
5
8
|
attr_accessor :PX_DEFAULT
|
6
|
-
attr_accessor :MONITOR_MODE
|
7
|
-
attr_accessor :ACTIVE_MODE
|
8
|
-
|
9
|
-
MONITOR_MODE = 1
|
10
|
-
ACTIVE_MODE = 2
|
11
9
|
|
12
10
|
PX_DEFAULT = {
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
11
|
+
:app_id => nil,
|
12
|
+
:cookie_key => nil,
|
13
|
+
:auth_token => nil,
|
14
|
+
:module_enabled => true,
|
15
|
+
:challenge_enabled => true,
|
16
|
+
:encryption_enabled => true,
|
17
|
+
:blocking_score => 100,
|
18
|
+
:sensitive_headers => ["http-cookie", "http-cookies"],
|
19
|
+
:api_connect_timeout => 1,
|
20
|
+
:api_timeout => 1,
|
21
|
+
:max_buffer_len => 10,
|
22
|
+
:send_page_activities => true,
|
23
|
+
:send_block_activities => true,
|
24
|
+
:sdk_name => PxModule::SDK_NAME,
|
25
|
+
:debug => false,
|
26
|
+
:module_mode => PxModule::MONITOR_MODE,
|
27
|
+
:local_proxy => false,
|
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
|
23
34
|
}
|
24
35
|
|
25
36
|
def initialize(params)
|
26
|
-
PX_DEFAULT[
|
27
|
-
@configuration = PX_DEFAULT.merge(params)
|
37
|
+
PX_DEFAULT[:backend_url] = "https://sapi-#{params[:app_id].downcase}.perimeterx.net"
|
38
|
+
@configuration = PX_DEFAULT.merge(params)
|
39
|
+
@configuration[:logger] = PxLogger.new(@configuration[:debug])
|
28
40
|
end
|
29
41
|
end
|
30
42
|
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'perimeterx/internal/clients/perimeter_x_risk_client'
|
2
|
+
|
3
|
+
module PxModule
|
4
|
+
class PerimeterxActivitiesClient < PerimeterxRiskClient
|
5
|
+
|
6
|
+
|
7
|
+
def initialize(px_config, http_client)
|
8
|
+
super(px_config, http_client)
|
9
|
+
@logger.debug("PerimeterxActivitiesClients[initialize]")
|
10
|
+
end
|
11
|
+
|
12
|
+
def send_to_perimeterx(activity_type, px_ctx, details = [])
|
13
|
+
@logger.debug("PerimeterxActivitiesClients[send_to_perimeterx]")
|
14
|
+
@logger.debug("PerimeterxActivitiesClients[send_to_perimeterx]: new activity #{activity_type} logged")
|
15
|
+
|
16
|
+
if (@px_config.key?(:additional_activity_handler))
|
17
|
+
@px_config[:additional_activity_handler].call(activity_type, px_ctx, details)
|
18
|
+
end
|
19
|
+
|
20
|
+
details[:module_version] = @px_config[:sdk_name]
|
21
|
+
details[:cookie_origin] = px_ctx.context[:cookie_origin]
|
22
|
+
|
23
|
+
px_data = {
|
24
|
+
:type => activity_type,
|
25
|
+
:headers => format_headers(px_ctx),
|
26
|
+
:timestamp => (Time.now.to_f*1000).floor,
|
27
|
+
:socket_ip => px_ctx.context[:ip],
|
28
|
+
:px_app_id => @px_config[:app_id],
|
29
|
+
:url => px_ctx.context[:full_url],
|
30
|
+
:details => details,
|
31
|
+
}
|
32
|
+
|
33
|
+
if (px_ctx.context.key?(:vid))
|
34
|
+
@logger.debug("PerimeterxActivitiesClients[send_to_perimeterx]: found vid in ctx")
|
35
|
+
px_data[:vid] = px_ctx.context[:vid]
|
36
|
+
end
|
37
|
+
|
38
|
+
# Prepare request
|
39
|
+
headers = {
|
40
|
+
"Authorization" => "Bearer #{@px_config[:auth_token]}" ,
|
41
|
+
"Content-Type" => "application/json"
|
42
|
+
};
|
43
|
+
|
44
|
+
s = Time.now
|
45
|
+
@http_client.async.post(PxModule::API_V1_S2S, px_data, headers)
|
46
|
+
e = Time.now
|
47
|
+
@logger.debug("PerimeterxActivitiesClients[send_to_perimeterx]: post runtime #{(e-s)*1000}")
|
48
|
+
end
|
49
|
+
|
50
|
+
def send_block_activity(px_ctx)
|
51
|
+
@logger.debug("PerimeterxActivitiesClients[send_block_activity]")
|
52
|
+
if (!@px_config[:send_block_activities])
|
53
|
+
@logger.debug("PerimeterxActivitiesClients[send_block_activity]: sending activites is disabled")
|
54
|
+
return
|
55
|
+
end
|
56
|
+
|
57
|
+
details = {
|
58
|
+
:http_version => px_ctx.context[:http_version],
|
59
|
+
:http_method => px_ctx.context[:http_method],
|
60
|
+
:client_uuid => px_ctx.context[:uuid],
|
61
|
+
:block_score => px_ctx.context[:score],
|
62
|
+
:block_reason => px_ctx.context[:blocking_reason],
|
63
|
+
:simulated_block => @px_config[:module_mode] == PxModule::MONITOR_MODE
|
64
|
+
}
|
65
|
+
|
66
|
+
if (px_ctx.context.key?(:risk_rtt))
|
67
|
+
details[:risk_rtt] = px_ctx.context[:risk_rtt]
|
68
|
+
end
|
69
|
+
|
70
|
+
if (px_ctx.context.key?(:px_orig_cookie))
|
71
|
+
details[:px_orig_cookie] = px_ctx.context[:px_orig_cookie]
|
72
|
+
end
|
73
|
+
|
74
|
+
send_to_perimeterx(PxModule::BLOCK_ACTIVITY, px_ctx, details)
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
def send_page_requested_activity(px_ctx)
|
79
|
+
@logger.debug("PerimeterxActivitiesClients[send_page_requested_activity]")
|
80
|
+
if (!@px_config[:send_page_activities])
|
81
|
+
return
|
82
|
+
end
|
83
|
+
|
84
|
+
details = {
|
85
|
+
:http_version => px_ctx.context[:http_version],
|
86
|
+
:http_method => px_ctx.context[:http_method],
|
87
|
+
:client_uuid => px_ctx.context[:uuid],
|
88
|
+
:pass_reason => px_ctx.context[:pass_reason]
|
89
|
+
}
|
90
|
+
|
91
|
+
if (px_ctx.context.key?(:decoded_cookie))
|
92
|
+
details[:px_cookie] = px_ctx.context[:decoded_cookie]
|
93
|
+
end
|
94
|
+
|
95
|
+
if (px_ctx.context.key?(:px_orig_cookie))
|
96
|
+
details[:px_orig_cookie] = px_ctx.context[:px_orig_cookie]
|
97
|
+
end
|
98
|
+
|
99
|
+
if (px_ctx.context.key?(:cookie_hmac))
|
100
|
+
details[:px_cookie_hmac] = px_ctx.context[:cookie_hmac]
|
101
|
+
end
|
102
|
+
|
103
|
+
if (px_ctx.context.key?(:risk_rtt))
|
104
|
+
details[:risk_rtt] = px_ctx.context[:risk_rtt]
|
105
|
+
end
|
106
|
+
|
107
|
+
send_to_perimeterx(PxModule::PAGE_REQUESTED_ACTIVITY, px_ctx, details)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'perimeterx/utils/px_logger'
|
2
|
+
|
3
|
+
module PxModule
|
4
|
+
class PerimeterxRiskClient
|
5
|
+
attr_accessor :px_config
|
6
|
+
attr_accessor :http_client
|
7
|
+
|
8
|
+
def initialize(px_config, http_client)
|
9
|
+
@px_config = px_config
|
10
|
+
@http_client = http_client;
|
11
|
+
@logger = px_config[:logger]
|
12
|
+
end
|
13
|
+
|
14
|
+
def format_headers(px_ctx)
|
15
|
+
@logger.debug("PerimeterxRiskClient[format_headers]")
|
16
|
+
formated_headers = []
|
17
|
+
px_ctx.context[:headers].each do |k,v|
|
18
|
+
if (!@px_config[:sensitive_headers].include? k.to_s)
|
19
|
+
formated_headers.push({
|
20
|
+
:name => k.to_s,
|
21
|
+
:value => v
|
22
|
+
})
|
23
|
+
end #end if
|
24
|
+
end #end forech
|
25
|
+
return formated_headers
|
26
|
+
end #end method
|
27
|
+
end #end class
|
28
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module PxModule
|
2
|
+
class PerimeterxCookieV1 < 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("PerimeterxCookieV1[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
|
+
base_hmac_str = "#{cookie_time}#{@decoded_cookie[:s][:a]}#{cookie_score}#{cookie_uuid}#{cookie_vid}"
|
32
|
+
|
33
|
+
hmac_str_withip = "#{base_hmac_str}#{@px_ctx.context[:ip]}#{@px_ctx.context[:user_agent]}"
|
34
|
+
|
35
|
+
hmac_str_withoutip = "#{base_hmac_str}#{@px_ctx.context[:user_agent]}"
|
36
|
+
|
37
|
+
return (hmac_valid?(hmac_str_withoutip, cookie_hmac) || hmac_valid?(hmac_str_withip, cookie_hmac))
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module PxModule
|
2
|
+
class PerimeterxCookieV3 < 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("PerimeterxCookieV3[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
|
+
hmac_string = "#{@px_cookie}#{@px_ctx.context[:user_agent]}"
|
34
|
+
return hmac_valid?(hmac_string, cookie_hmac)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'active_support/security_utils'
|
2
|
+
require 'base64'
|
3
|
+
require 'openssl'
|
4
|
+
require 'perimeterx/internal/exceptions/px_cookie_decryption_exception'
|
5
|
+
|
6
|
+
module PxModule
|
7
|
+
class PerimeterxPayload
|
8
|
+
attr_accessor :px_cookie, :px_config, :px_ctx, :cookie_secret, :decoded_cookie
|
9
|
+
|
10
|
+
def initialize(px_config)
|
11
|
+
@px_config = px_config
|
12
|
+
@logger = px_config[:logger]
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.px_cookie_factory(px_ctx, px_config)
|
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))
|
22
|
+
return PerimeterxCookieV3.new(px_config, px_ctx)
|
23
|
+
end
|
24
|
+
return PerimeterxCookieV1.new(px_config, px_ctx)
|
25
|
+
end
|
26
|
+
|
27
|
+
def cookie_score
|
28
|
+
#abstract, must be implemented
|
29
|
+
raise Exception.new("Unimplemented method")
|
30
|
+
end
|
31
|
+
|
32
|
+
def cookie_hmac
|
33
|
+
#abstract, must be implemented
|
34
|
+
raise Exception.new("Unimplemented method")
|
35
|
+
end
|
36
|
+
|
37
|
+
def valid_format?(cookie)
|
38
|
+
#abstract, must be implemented
|
39
|
+
raise Exception.new("Unimplemented method")
|
40
|
+
end
|
41
|
+
|
42
|
+
def cookie_block_action
|
43
|
+
#abstract, must be implemented
|
44
|
+
raise Exception.new("Unimplemented method")
|
45
|
+
end
|
46
|
+
|
47
|
+
def secured?
|
48
|
+
#abstract, must be implemented
|
49
|
+
raise Exception.new("Unimplemented method")
|
50
|
+
end
|
51
|
+
|
52
|
+
def is_valid?
|
53
|
+
return deserialize && !expired? && secured?
|
54
|
+
end
|
55
|
+
|
56
|
+
def cookie_time
|
57
|
+
return @decoded_cookie[:t]
|
58
|
+
end
|
59
|
+
|
60
|
+
def cookie_uuid
|
61
|
+
return @decoded_cookie[:u]
|
62
|
+
end
|
63
|
+
|
64
|
+
def cookie_vid
|
65
|
+
return @decoded_cookie[:v]
|
66
|
+
end
|
67
|
+
|
68
|
+
def high_score?
|
69
|
+
return cookie_score >= @px_config[:blocking_score]
|
70
|
+
end
|
71
|
+
|
72
|
+
def expired?
|
73
|
+
return cookie_time < (Time.now.to_f*1000).floor
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
def deserialize
|
78
|
+
if (!@decoded_cookie.nil?)
|
79
|
+
return true
|
80
|
+
end
|
81
|
+
|
82
|
+
# Decode or decrypt, depends on configuration
|
83
|
+
if (@px_config[:encryption_enabled])
|
84
|
+
cookie = decrypt(@px_cookie)
|
85
|
+
else
|
86
|
+
cookie = decode(@px_cookie)
|
87
|
+
end
|
88
|
+
|
89
|
+
if (cookie.nil?)
|
90
|
+
return false
|
91
|
+
end
|
92
|
+
|
93
|
+
if (!valid_format?(cookie))
|
94
|
+
return false
|
95
|
+
end
|
96
|
+
|
97
|
+
@decoded_cookie = cookie
|
98
|
+
|
99
|
+
return true
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
def decrypt(px_cookie)
|
104
|
+
begin
|
105
|
+
if (px_cookie.nil?)
|
106
|
+
return
|
107
|
+
end
|
108
|
+
px_cookie = px_cookie.gsub(' ', '+')
|
109
|
+
salt, iterations, cipher_text = px_cookie.split(':')
|
110
|
+
iterations = iterations.to_i
|
111
|
+
if (iterations > @px_config[:risk_cookie_max_iterations] || iterations < 500)
|
112
|
+
return
|
113
|
+
end
|
114
|
+
salt = Base64.decode64(salt)
|
115
|
+
cipher_text = Base64.decode64(cipher_text)
|
116
|
+
digest = OpenSSL::Digest::SHA256.new
|
117
|
+
value = OpenSSL::PKCS5.pbkdf2_hmac(@px_config[:cookie_key], salt, iterations, 48, digest)
|
118
|
+
key = value[0..31]
|
119
|
+
iv = value[32..-1]
|
120
|
+
cipher = OpenSSL::Cipher::AES256.new(:CBC)
|
121
|
+
cipher.decrypt
|
122
|
+
cipher.key = key
|
123
|
+
cipher.iv = iv
|
124
|
+
plaintext = cipher.update(cipher_text) + cipher.final
|
125
|
+
|
126
|
+
return eval(plaintext)
|
127
|
+
rescue Exception => e
|
128
|
+
@logger.debug("PerimeterxCookie[decrypt]: Cookie decrypt fail #{e.message}")
|
129
|
+
raise PxCookieDecryptionException.new("Cookie decrypt fail => #{e.message}");
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def decode(px_cookie)
|
134
|
+
return eval(Base64.decode64(px_cookie))
|
135
|
+
end
|
136
|
+
|
137
|
+
|
138
|
+
def hmac_valid?(hmac_str, cookie_hmac)
|
139
|
+
hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, @cookie_secret, hmac_str)
|
140
|
+
# ref: https://thisdata.com/blog/timing-attacks-against-string-comparison/
|
141
|
+
password_correct = ActiveSupport::SecurityUtils.secure_compare(
|
142
|
+
::Digest::SHA256.hexdigest(cookie_hmac),
|
143
|
+
::Digest::SHA256.hexdigest(hmac)
|
144
|
+
)
|
145
|
+
|
146
|
+
end
|
147
|
+
end
|
148
|
+
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
|