perimeter_x 1.0.6.pre.alpha → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +5 -3
- data/Dockerfile +5 -3
- data/Gemfile +1 -1
- data/Gemfile.lock +44 -2
- data/LICENSE.txt +9 -12
- data/Rakefile +9 -2
- data/changelog.md +20 -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 +109 -33
- data/lib/perimeterx/configuration.rb +25 -17
- data/lib/perimeterx/internal/clients/perimeter_x_activity_client.rb +92 -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/perimeter_x_context.rb +81 -53
- data/lib/perimeterx/internal/perimeter_x_cookie.rb +140 -0
- data/lib/perimeterx/internal/perimeter_x_cookie_v1.rb +42 -0
- data/lib/perimeterx/internal/perimeter_x_cookie_v3.rb +37 -0
- data/lib/perimeterx/internal/validators/perimeter_x_captcha_validator.rb +65 -0
- data/lib/perimeterx/internal/validators/perimeter_x_cookie_validator.rb +76 -0
- data/lib/perimeterx/internal/validators/perimeter_x_s2s_validator.rb +114 -0
- data/lib/perimeterx/utils/px_constants.rb +45 -0
- data/lib/perimeterx/utils/px_http_client.rb +47 -26
- data/lib/perimeterx/utils/px_logger.rb +12 -6
- data/lib/perimeterx/utils/px_template_factory.rb +31 -0
- data/lib/perimeterx/utils/templates/block.mustache +146 -0
- data/lib/perimeterx/utils/templates/captcha.mustache +185 -0
- data/lib/perimeterx/version.rb +2 -2
- data/perimeter_x.gemspec +6 -1
- data/readme.md +218 -34
- metadata +90 -11
- 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
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'perimeterx/internal/clients/perimeter_x_risk_client'
|
2
|
+
|
3
|
+
module PxModule
|
4
|
+
class PerimeterxActivitiesClient < PerimeterxRiskClient
|
5
|
+
|
6
|
+
attr_accessor :activities
|
7
|
+
|
8
|
+
def initialize(px_config, http_client)
|
9
|
+
super(px_config, http_client)
|
10
|
+
@logger.debug("PerimeterxActivitiesClients[initialize]")
|
11
|
+
@activities = [];
|
12
|
+
end
|
13
|
+
|
14
|
+
def send_to_perimeterx(activity_type, px_ctx, details = [])
|
15
|
+
@logger.debug("PerimeterxActivitiesClients[send_to_perimeterx]")
|
16
|
+
@logger.debug("PerimeterxActivitiesClients[send_to_perimeterx]: new activity #{activity_type} logged")
|
17
|
+
|
18
|
+
if (@px_config.key?(:additional_activity_handler))
|
19
|
+
@px_config[:additional_activity_handler].call(activity_type, px_ctx, details)
|
20
|
+
end
|
21
|
+
|
22
|
+
details[:module_version] = @px_config[:sdk_name]
|
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
|
+
@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
|
51
|
+
end
|
52
|
+
|
53
|
+
def send_block_activity(px_ctx)
|
54
|
+
@logger.debug("PerimeterxActivitiesClients[send_block_activity]")
|
55
|
+
if (!@px_config[:send_page_acitivites])
|
56
|
+
@logger.debug("PerimeterxActivitiesClients[send_block_activity]: sending activites is disabled")
|
57
|
+
return
|
58
|
+
end
|
59
|
+
|
60
|
+
details = {
|
61
|
+
:block_uuid => px_ctx.context[:uuid],
|
62
|
+
:block_score => px_ctx.context[:score],
|
63
|
+
:block_reason => px_ctx.context[:block_reason]
|
64
|
+
}
|
65
|
+
|
66
|
+
send_to_perimeterx(PxModule::BLOCK_ACTIVITY, px_ctx, details)
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
def send_page_requested_activity(px_ctx)
|
71
|
+
@logger.debug("PerimeterxActivitiesClients[send_page_requested_activity]")
|
72
|
+
if (!@px_config[:send_page_acitivites])
|
73
|
+
return
|
74
|
+
end
|
75
|
+
|
76
|
+
details = {
|
77
|
+
:http_version => px_ctx.context[:http_version],
|
78
|
+
:http_method => px_ctx.context[:http_method]
|
79
|
+
}
|
80
|
+
|
81
|
+
if (px_ctx.context.key?(:decoded_cookie))
|
82
|
+
details[:px_cookie] = px_ctx.context[:decoded_cookie]
|
83
|
+
end
|
84
|
+
|
85
|
+
if (px_ctx.context.key?(:cookie_hmac))
|
86
|
+
details[:px_cookie_hmac] = px_ctx.context[:cookie_hmac]
|
87
|
+
end
|
88
|
+
|
89
|
+
send_to_perimeterx(PxModule::PAGE_REQUESTED_ACTIVITY, px_ctx, details)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
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
|
@@ -1,62 +1,90 @@
|
|
1
1
|
require 'perimeterx/utils/px_logger'
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
3
|
+
module PxModule
|
4
|
+
class PerimeterXContext
|
5
|
+
|
6
|
+
attr_accessor :context
|
7
|
+
attr_accessor :px_config
|
8
|
+
|
9
|
+
def initialize(px_config, req)
|
10
|
+
@logger = px_config[:logger];
|
11
|
+
@logger.debug("PerimeterXContext[initialize] ")
|
12
|
+
@context = Hash.new
|
13
|
+
|
14
|
+
@context[:px_cookie] = Hash.new
|
15
|
+
@context[:headers] = Hash.new
|
16
|
+
cookies = req.cookies
|
17
|
+
if (!cookies.empty?)
|
18
|
+
# Prepare hashed cookies
|
19
|
+
cookies.each do |k, v|
|
20
|
+
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
|
27
|
+
end
|
28
|
+
end #end case
|
29
|
+
end #end empty cookies
|
30
|
+
|
31
|
+
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
|
26
36
|
end
|
27
|
-
end #end
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
37
|
+
end #end headers foreach
|
38
|
+
|
39
|
+
@context[:hostname]= req.server_name
|
40
|
+
@context[:user_agent] = req.user_agent ? req.user_agent : ''
|
41
|
+
@context[:uri] = px_config[:custom_uri] ? px_config[:custom_uri].call(req) : req.fullpath
|
42
|
+
@context[:full_url] = req.original_url
|
43
|
+
@context[:format] = req.format.symbol
|
44
|
+
@context[:score] = 0
|
45
|
+
|
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
|
35
52
|
end
|
36
|
-
end #end headers foreach
|
37
|
-
|
38
|
-
@context[:hostname]= req.headers['HTTP_HOST']
|
39
|
-
@context[:user_agent] = req.headers['HTTP_USER_AGENT'] ? req.headers['HTTP_USER_AGENT'] : ''
|
40
|
-
@context[:full_url] = req.original_url
|
41
|
-
@context[:score] = 0
|
42
|
-
|
43
|
-
if px_config.key?('custom_user_ip')
|
44
|
-
@context[:ip] = px_config['custom_user_ip']
|
45
|
-
elsif px_config.key?('px_custom_user_ip_method')
|
46
|
-
puts "px_custom_user_ip_method triggered"
|
47
|
-
@context[:ip] = px_config['px_custom_user_ip_method'].call(req)
|
48
|
-
else
|
49
|
-
@context[:ip] = req.headers['REMOTE_ADDR'];
|
50
|
-
end
|
51
53
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
54
|
+
if req.server_protocol
|
55
|
+
httpVer = req.server_protocol.split("/")
|
56
|
+
if httpVer.size > 0
|
57
|
+
@context[:http_version] = httpVer[1];
|
58
|
+
end
|
56
59
|
end
|
60
|
+
@context[:http_method] = req.method
|
61
|
+
@context[:sensitive_route] = check_sensitive_route(px_config[:sensitive_routes], @context[:uri])
|
62
|
+
end #end init
|
63
|
+
|
64
|
+
def check_sensitive_route(sensitive_routes, uri)
|
65
|
+
sensitive_routes.each do |sensitive_route|
|
66
|
+
return true if uri.start_with? sensitive_route
|
67
|
+
end
|
68
|
+
return false
|
69
|
+
end
|
70
|
+
|
71
|
+
def set_block_action_type(action)
|
72
|
+
@context[:block_action] = case action
|
73
|
+
when "c"
|
74
|
+
"captcha"
|
75
|
+
when "b"
|
76
|
+
return "block"
|
77
|
+
when "j"
|
78
|
+
return "challenge"
|
79
|
+
else
|
80
|
+
return "captcha"
|
81
|
+
end
|
57
82
|
end
|
58
|
-
@context[:http_method] = req.headers['REQUEST_METHOD'];
|
59
83
|
|
60
|
-
|
84
|
+
def get_px_cookie
|
85
|
+
cookie = @context[:px_cookie].key?(:v3) ? @context[:px_cookie][:v3] : @context[:px_cookie][:v1]
|
86
|
+
return cookie.tr(' ','+') if !cookie.nil?
|
87
|
+
end
|
61
88
|
|
62
|
-
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,140 @@
|
|
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 PerimeterxCookie
|
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[:px_cookie].key?(:v3))
|
17
|
+
return PerimeterxCookieV3.new(px_config, px_ctx)
|
18
|
+
end
|
19
|
+
return PerimeterxCookieV1.new(px_config, px_ctx)
|
20
|
+
end
|
21
|
+
|
22
|
+
def cookie_score
|
23
|
+
#abstract, must be implemented
|
24
|
+
raise Exception.new("Unimplemented method")
|
25
|
+
end
|
26
|
+
|
27
|
+
def cookie_hmac
|
28
|
+
#abstract, must be implemented
|
29
|
+
raise Exception.new("Unimplemented method")
|
30
|
+
end
|
31
|
+
|
32
|
+
def valid_format?(cookie)
|
33
|
+
#abstract, must be implemented
|
34
|
+
raise Exception.new("Unimplemented method")
|
35
|
+
end
|
36
|
+
|
37
|
+
def cookie_block_action
|
38
|
+
#abstract, must be implemented
|
39
|
+
raise Exception.new("Unimplemented method")
|
40
|
+
end
|
41
|
+
|
42
|
+
def secured?
|
43
|
+
#abstract, must be implemented
|
44
|
+
raise Exception.new("Unimplemented method")
|
45
|
+
end
|
46
|
+
|
47
|
+
def is_valid?
|
48
|
+
return deserialize && !expired? && secured?
|
49
|
+
end
|
50
|
+
|
51
|
+
def cookie_time
|
52
|
+
return @decoded_cookie[:t]
|
53
|
+
end
|
54
|
+
|
55
|
+
def cookie_uuid
|
56
|
+
return @decoded_cookie[:u]
|
57
|
+
end
|
58
|
+
|
59
|
+
def cookie_vid
|
60
|
+
return @decoded_cookie[:v]
|
61
|
+
end
|
62
|
+
|
63
|
+
def high_score?
|
64
|
+
return cookie_score >= @px_config[:blocking_score]
|
65
|
+
end
|
66
|
+
|
67
|
+
def expired?
|
68
|
+
return cookie_time < (Time.now.to_f*1000).floor
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
def deserialize
|
73
|
+
if (!@decoded_cookie.nil?)
|
74
|
+
return true
|
75
|
+
end
|
76
|
+
|
77
|
+
# Decode or decrypt, depends on configuration
|
78
|
+
if (@px_config[:encryption_enabled])
|
79
|
+
cookie = decrypt(@px_cookie)
|
80
|
+
else
|
81
|
+
cookie = decode(@px_cookie)
|
82
|
+
end
|
83
|
+
|
84
|
+
if (cookie.nil?)
|
85
|
+
return false
|
86
|
+
end
|
87
|
+
|
88
|
+
if (!valid_format?(cookie))
|
89
|
+
return false
|
90
|
+
end
|
91
|
+
|
92
|
+
@decoded_cookie = cookie
|
93
|
+
|
94
|
+
return true
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
def decrypt(px_cookie)
|
99
|
+
begin
|
100
|
+
if (px_cookie.nil?)
|
101
|
+
return
|
102
|
+
end
|
103
|
+
px_cookie = px_cookie.gsub(' ', '+')
|
104
|
+
salt, iterations, cipher_text = px_cookie.split(':')
|
105
|
+
iterations = iterations.to_i
|
106
|
+
salt = Base64.decode64(salt)
|
107
|
+
cipher_text = Base64.decode64(cipher_text)
|
108
|
+
digest = OpenSSL::Digest::SHA256.new
|
109
|
+
value = OpenSSL::PKCS5.pbkdf2_hmac(@px_config[:cookie_key], salt, iterations, 48, digest)
|
110
|
+
key = value[0..31]
|
111
|
+
iv = value[32..-1]
|
112
|
+
cipher = OpenSSL::Cipher::AES256.new(:CBC)
|
113
|
+
cipher.decrypt
|
114
|
+
cipher.key = key
|
115
|
+
cipher.iv = iv
|
116
|
+
plaintext = cipher.update(cipher_text) + cipher.final
|
117
|
+
|
118
|
+
return eval(plaintext)
|
119
|
+
rescue Exception => e
|
120
|
+
@logger.debug("PerimeterxCookie[decrypt]: Cookie decrypt fail #{e.message}")
|
121
|
+
raise PxCookieDecryptionException.new("Cookie decrypt fail => #{e.message}");
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def decode(px_cookie)
|
126
|
+
return eval(Base64.decode64(px_cookie))
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
def hmac_valid?(hmac_str, cookie_hmac)
|
131
|
+
hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, @cookie_secret, hmac_str)
|
132
|
+
# ref: https://thisdata.com/blog/timing-attacks-against-string-comparison/
|
133
|
+
password_correct = ActiveSupport::SecurityUtils.secure_compare(
|
134
|
+
::Digest::SHA256.hexdigest(cookie_hmac),
|
135
|
+
::Digest::SHA256.hexdigest(hmac)
|
136
|
+
)
|
137
|
+
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module PxModule
|
2
|
+
class PerimeterxCookieV1 < PerimeterxCookie
|
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
|