perimeter_x 1.0.6.pre.alpha → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|