perimeter_x 1.4.0 → 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.
@@ -108,6 +108,9 @@ module PxModule
108
108
  px_cookie = px_cookie.gsub(' ', '+')
109
109
  salt, iterations, cipher_text = px_cookie.split(':')
110
110
  iterations = iterations.to_i
111
+ if (iterations > @px_config[:risk_cookie_max_iterations] || iterations < 500)
112
+ return
113
+ end
111
114
  salt = Base64.decode64(salt)
112
115
  cipher_text = Base64.decode64(cipher_text)
113
116
  digest = OpenSSL::Digest::SHA256.new
@@ -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
@@ -17,27 +18,45 @@ module PxModule
17
18
  @context[:made_s2s_risk_api_call] = false
18
19
  cookies = req.cookies
19
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
+
20
36
  # Get token from header
21
37
  if req.headers[PxModule::TOKEN_HEADER]
22
38
  @context[:cookie_origin] = 'header'
23
- token = req.headers[PxModule::TOKEN_HEADER]
39
+ token = force_utf8(req.headers[PxModule::TOKEN_HEADER])
24
40
  if token.include? ':'
25
41
  exploded_token = token.split(':', 2)
26
42
  cookie_sym = "v#{exploded_token[0]}".to_sym
27
43
  @context[:px_cookie][cookie_sym] = exploded_token[1]
28
44
  else # TOKEN_HEADER exists yet there's no ':' delimiter - may indicate an error (storing original value)
29
- @context[:px_cookie] = req.headers[PxModule::TOKEN_HEADER]
45
+ @context[:px_cookie] = force_utf8(req.headers[PxModule::TOKEN_HEADER])
30
46
  end
31
47
  elsif !cookies.empty? # Get cookie from jar
32
48
  # Prepare hashed cookies
33
49
  cookies.each do |k, v|
34
50
  case k.to_s
35
51
  when '_px3'
36
- @context[:px_cookie][:v3] = v
52
+ @context[:px_cookie][:v3] = force_utf8(v)
37
53
  when '_px'
38
- @context[:px_cookie][:v1] = v
39
- when '_pxCaptcha'
40
- @context[:px_captcha] = v
54
+ @context[:px_cookie][:v1] = force_utf8(v)
55
+ when '_pxvid'
56
+ if v.is_a?(String) && v.match(PxModule::VID_REGEX)
57
+ @context[:vid_source] = "vid_cookie"
58
+ @context[:vid] = force_utf8(v)
59
+ end
41
60
  end
42
61
  end #end case
43
62
  end #end empty cookies
@@ -46,7 +65,7 @@ module PxModule
46
65
  if (k.start_with? 'HTTP_')
47
66
  header = k.to_s.gsub('HTTP_', '')
48
67
  header = header.gsub('_', '-').downcase
49
- @context[:headers][header.to_sym] = v
68
+ @context[:headers][header.to_sym] = force_utf8(v)
50
69
  end
51
70
  end #end headers foreach
52
71
 
@@ -57,14 +76,6 @@ module PxModule
57
76
  @context[:format] = req.format.symbol
58
77
  @context[:score] = 0
59
78
 
60
- if px_config.key?(:custom_user_ip)
61
- @context[:ip] = req.headers[px_config[:custom_user_ip]]
62
- elsif px_config.key?(:px_custom_user_ip_method)
63
- @context[:ip] = px_config[:px_custom_user_ip_method].call(req)
64
- else
65
- @context[:ip] = req.ip
66
- end
67
-
68
79
  if req.server_protocol
69
80
  httpVer = req.server_protocol.split('/')
70
81
  if httpVer.size > 0
@@ -82,6 +93,10 @@ module PxModule
82
93
  false
83
94
  end
84
95
 
96
+ def force_utf8(str)
97
+ return str.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '')
98
+ end
99
+
85
100
  def set_block_action_type(action)
86
101
  @context[:block_action] = case action
87
102
  when 'c'
@@ -90,6 +105,8 @@ module PxModule
90
105
  return 'block'
91
106
  when 'j'
92
107
  return 'challenge'
108
+ when 'r'
109
+ return 'rate_limit'
93
110
  else
94
111
  return captcha
95
112
  end
@@ -47,16 +47,22 @@ module PxModule
47
47
  cookie = PerimeterxPayload.px_cookie_factory(px_ctx, @px_config)
48
48
  if !cookie.deserialize()
49
49
  @logger.warn("PerimeterxCookieValidator:[verify]: invalid cookie")
50
+ px_ctx.context[:px_orig_cookie] = px_ctx.get_px_cookie
50
51
  px_ctx.context[:s2s_call_reason] = PxModule::COOKIE_DECRYPTION_FAILED
51
52
  return false, px_ctx
52
53
  end
53
54
  px_ctx.context[:decoded_cookie] = cookie.decoded_cookie
54
55
  px_ctx.context[:score] = cookie.cookie_score()
55
56
  px_ctx.context[:uuid] = cookie.decoded_cookie[:u]
56
- px_ctx.context[:vid] = cookie.decoded_cookie[:v]
57
57
  px_ctx.context[:block_action] = px_ctx.set_block_action_type(cookie.cookie_block_action())
58
58
  px_ctx.context[:cookie_hmac] = cookie.cookie_hmac()
59
59
 
60
+ vid = cookie.decoded_cookie[:v]
61
+ if vid.is_a?(String) && vid.match(PxModule::VID_REGEX)
62
+ px_ctx.context[:vid_source] = "risk_cookie"
63
+ px_ctx.context[:vid] = vid
64
+ end
65
+
60
66
  if cookie.expired?
61
67
  @logger.warn("PerimeterxCookieValidator:[verify]: cookie expired")
62
68
  px_ctx.context[:s2s_call_reason] = PxModule::EXPIRED_COOKIE
@@ -83,10 +89,11 @@ module PxModule
83
89
 
84
90
  @logger.debug("PerimeterxCookieValidator:[verify]: cookie validation passed succesfully")
85
91
 
92
+ px_ctx.context[:pass_reason] = 'cookie'
86
93
  return true, px_ctx
87
94
  rescue Exception => e
88
95
  @logger.error("PerimeterxCookieValidator:[verify]: exception while verifying cookie => #{e.message}")
89
- px_ctx.context[:px_orig_cookie] = cookie.px_cookie
96
+ px_ctx.context[:px_orig_cookie] = px_ctx.context[:px_cookie]
90
97
  px_ctx.context[:s2s_call_reason] = PxModule::COOKIE_DECRYPTION_FAILED
91
98
  return false, px_ctx
92
99
  end
@@ -67,11 +67,19 @@ module PxModule
67
67
  };
68
68
 
69
69
  # Custom risk handler
70
+ risk_start = Time.now
70
71
  if (risk_mode == PxModule::ACTIVE_MODE && @px_config.key?(:custom_risk_handler))
71
- response = @px_config[:custom_risk_handler].call(PxModule::API_V2_RISK, request_body, headers, @px_config[:api_timeout], @px_config[:api_timeout_connection])
72
+ response = @px_config[:custom_risk_handler].call(PxModule::API_V3_RISK, request_body, headers, @px_config[:api_timeout], @px_config[:api_timeout_connection])
72
73
  else
73
- response = @http_client.post(PxModule::API_V2_RISK , request_body, headers, @px_config[:api_timeout], @px_config[:api_timeout_connection])
74
+ response = @http_client.post(PxModule::API_V3_RISK , request_body, headers, @px_config[:api_timeout], @px_config[:api_timeout_connection])
74
75
  end
76
+
77
+ # Set risk_rtt
78
+ if(response)
79
+ risk_end = Time.now
80
+ px_ctx.context[:risk_rtt] = ((risk_end-risk_start)*1000).round
81
+ end
82
+
75
83
  return response
76
84
  end
77
85
 
@@ -79,6 +87,7 @@ module PxModule
79
87
  @logger.debug("PerimeterxS2SValidator[verify]")
80
88
  response = send_risk_request(px_ctx)
81
89
  if (!response)
90
+ px_ctx.context[:pass_reason] = "s2s_timeout"
82
91
  return px_ctx
83
92
  end
84
93
  px_ctx.context[:made_s2s_risk_api_call] = true
@@ -86,7 +95,7 @@ module PxModule
86
95
  # From here response should be valid, if success or error
87
96
  response_body = eval(response.body);
88
97
  # When success
89
- if (response.code == 200 && response_body.key?(:score) && response_body.key?(:action))
98
+ if (response.code == 200 && response_body.key?(:score) && response_body.key?(:action) && response_body.key?(:status) && response_body[:status] == 0 )
90
99
  @logger.debug("PerimeterxS2SValidator[verify]: response ok")
91
100
  score = response_body[:score]
92
101
  px_ctx.context[:score] = score
@@ -97,13 +106,17 @@ module PxModule
97
106
  px_ctx.context[:blocking_reason] = 'challenge'
98
107
  elsif (score >= @px_config[:blocking_score])
99
108
  px_ctx.context[:blocking_reason] = 's2s_high_score'
109
+ else
110
+ px_ctx.context[:pass_reason] = 's2s'
100
111
  end #end challange or blocking score
101
112
  end #end success response
102
113
 
103
114
  # When error
104
- if(response.code != 200)
105
- @logger.warn("PerimeterxS2SValidator[verify]: bad response, return code #{response.code}")
106
- px_ctx.context[:uuid] = ""
115
+ risk_error_status = response_body && response_body.key?(:status) && response_body[:status] == -1
116
+ if(response.code != 200 || risk_error_status)
117
+ @logger.warn("PerimeterxS2SValidator[verify]: bad response, returned code #{response.code} #{risk_error_status ? "risk status: -1" : ""}")
118
+ px_ctx.context[:pass_reason] = 'request_failed'
119
+ px_ctx.context[:uuid] = (!response_body || response_body[:uuid].nil?)? "" : response_body[:uuid]
107
120
  px_ctx.context[:s2s_error_msg] = !response_body || response_body[:message].nil? ? 'unknown' : response_body[:message]
108
121
  end
109
122
 
@@ -10,8 +10,7 @@ module PxModule
10
10
 
11
11
  # Routes
12
12
  API_V1_S2S = '/api/v1/collector/s2s'
13
- API_CAPTCHA = '/api/v2/risk/captcha'
14
- API_V2_RISK = '/api/v2/risk'
13
+ API_V3_RISK = '/api/v3/risk'
15
14
 
16
15
  # Activity Types
17
16
  BLOCK_ACTIVITY = 'block'
@@ -27,8 +26,9 @@ module PxModule
27
26
  SENSITIVE_ROUTE = 'sensitive_route'
28
27
 
29
28
  # Templates
30
- BLOCK_TEMPLATE = 'block'
29
+ CHALLENGE_TEMPLATE = 'block_template'
31
30
  TEMPLATE_EXT = '.mustache'
31
+ RATELIMIT_TEMPLATE = 'ratelimit'
32
32
 
33
33
 
34
34
  # Template Props
@@ -40,7 +40,14 @@ module PxModule
40
40
  PROP_CUSTOM_LOGO = :customLogo
41
41
  PROP_CSS_REF = :cssRef
42
42
  PROP_JS_REF = :jsRef
43
- HOST_URL = :hostUrl
43
+ PROP_BLOCK_SCRIPT = :blockScript
44
+ PROP_JS_CLIENT_SRC = :jsClientSrc
45
+ PROP_HOST_URL = :hostUrl
46
+ PROP_FIRST_PARTY_ENABLED = :firstPartyEnabled
47
+
48
+ # Hosts
49
+ CLIENT_HOST = 'client.px-cloud.net'
50
+ CAPTCHA_HOST = 'captcha.px-cloud.net'
44
51
 
45
52
  VISIBLE = 'visible'
46
53
  HIDDEN = 'hidden'
@@ -49,4 +56,7 @@ module PxModule
49
56
  TOKEN_HEADER = 'X-PX-AUTHORIZATION'
50
57
  MOBILE_SDK_CONNECTION_ERROR = 'mobile_sdk_connection_error'
51
58
  MOBILE_SDK_PINNING_ERROR = 'mobile_sdk_pinning_error'
59
+
60
+ # Regular Expressions
61
+ VID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
52
62
  end
@@ -12,7 +12,7 @@ module PxModule
12
12
  def initialize(px_config)
13
13
  @px_config = px_config
14
14
  @logger = px_config[:logger]
15
- @logger.debug("PxHttpClient[initialize]: HTTP client is being initilized with base_uri: #{px_config[:perimeterx_server_host]}")
15
+ @logger.debug("PxHttpClient[initialize]: HTTP client is being initilized with base_uri: #{px_config[:backend_url]}")
16
16
  end
17
17
 
18
18
  # Runs a POST command to Perimeter X servers
@@ -28,7 +28,7 @@ module PxModule
28
28
  begin
29
29
  @logger.debug("PxHttpClient[post]: posting to #{path} headers {#{headers.to_json()}} body: {#{body.to_json()}} ")
30
30
  response = Typhoeus.post(
31
- "#{px_config[:perimeterx_server_host]}#{path}",
31
+ "#{px_config[:backend_url]}#{path}",
32
32
  headers: headers,
33
33
  body: body.to_json,
34
34
  timeout: api_timeout,
@@ -3,23 +3,24 @@ require 'perimeterx/utils/px_constants'
3
3
  module PxModule
4
4
  module PxTemplateFactory
5
5
 
6
- def self.get_template(px_ctx, px_config)
6
+ def self.get_template(px_ctx, px_config, px_template_object)
7
7
  logger = px_config[:logger]
8
8
  if (px_config[:challenge_enabled] && px_ctx.context[:block_action] == 'challenge')
9
9
  logger.debug('PxTemplateFactory[get_template]: px challange triggered')
10
10
  return px_ctx.context[:block_action_data].html_safe
11
11
  end
12
12
 
13
- logger.debug('PxTemplateFactory[get_template]: rendering template')
14
- template_type = px_ctx.context[:block_action] == 'captcha' ? px_config[:captcha_provider].downcase : BLOCK_TEMPLATE
13
+ view = Mustache.new
15
14
 
16
- template_postfix = ''
17
- if px_ctx.context[:cookie_origin] == 'header'
18
- template_postfix = '.mobile'
15
+ if (px_ctx.context[:block_action] == 'rate_limit')
16
+ logger.debug('PxTemplateFactory[get_template]: rendering ratelimit template')
17
+ template_type = RATELIMIT_TEMPLATE
18
+ else
19
+ logger.debug('PxTemplateFactory[get_template]: rendering template')
20
+ template_type = CHALLENGE_TEMPLATE
19
21
  end
20
22
 
21
- Mustache.template_file = "#{File.dirname(__FILE__) }/templates/#{template_type}#{template_postfix}#{PxModule::TEMPLATE_EXT}"
22
- view = Mustache.new
23
+ Mustache.template_file = "#{File.dirname(__FILE__) }/templates/#{template_type}#{PxModule::TEMPLATE_EXT}"
23
24
 
24
25
  view[PxModule::PROP_APP_ID] = px_config[:app_id]
25
26
  view[PxModule::PROP_REF_ID] = px_ctx.context[:uuid]
@@ -28,8 +29,11 @@ module PxModule
28
29
  view[PxModule::PROP_CUSTOM_LOGO] = px_config[:custom_logo]
29
30
  view[PxModule::PROP_CSS_REF] = px_config[:css_ref]
30
31
  view[PxModule::PROP_JS_REF] = px_config[:js_ref]
31
- view[PxModule::HOST_URL] = "https://collector-#{px_config[:app_id]}.perimeterx.net"
32
+ view[PxModule::PROP_HOST_URL] = "https://collector-#{px_config[:app_id]}.perimeterx.net"
32
33
  view[PxModule::PROP_LOGO_VISIBILITY] = px_config[:custom_logo] ? PxModule::VISIBLE : PxModule::HIDDEN
34
+ view[PxModule::PROP_BLOCK_SCRIPT] = px_template_object[:block_script]
35
+ view[PxModule::PROP_JS_CLIENT_SRC] = px_template_object[:js_client_src]
36
+ view[PxModule::PROP_FIRST_PARTY_ENABLED] = false
33
37
 
34
38
  return view.render.html_safe
35
39
  end
@@ -86,10 +86,9 @@
86
86
  }
87
87
  </style>
88
88
  <!-- Custom CSS -->
89
- {{# cssRef }}
90
- <link rel="stylesheet" type="text/css" href="{{ . }}"/>
91
- {{/ cssRef }}
92
- <script src="https://funcaptcha.com/fc/api/?onload=loadFunCaptcha" async defer></script>
89
+ {{#cssRef}}
90
+ <link rel="stylesheet" type="text/css" href="{{{cssRef}}}"/>
91
+ {{/cssRef}}
93
92
  </head>
94
93
 
95
94
  <body>
@@ -106,10 +105,9 @@
106
105
  </div>
107
106
  <div class="content-wrapper">
108
107
  <div class="content">
109
- <p>
110
- Please click "Verify" to continue
111
- </p>
112
- <div id="CAPTCHA"></div>
108
+
109
+ <div id="px-captcha">
110
+ </div>
113
111
  <p>
114
112
  Access to this page has been denied because we believe you are using automation tools to browse the
115
113
  website.
@@ -144,35 +142,34 @@
144
142
  </div>
145
143
  </div>
146
144
  </section>
147
- <!-- Captcha -->
145
+ <!-- Px -->
148
146
  <script>
149
-
150
- function loadFunCaptcha() {
151
- var vid = '{{vid}}';
152
- var uuid = '{{uuid}}';
153
-
154
- new FunCaptcha({
155
- public_key: "19E4B3B8-6CBE-35CC-4205-FC79ECDDA765",
156
- target_html: "CAPTCHA",
157
- callback: function () {
158
- var expiryUtc = new Date(Date.now() + 1000 * 10).toUTCString();
159
- var pxCaptcha = "_pxCaptcha=" + btoa(JSON.stringify({r: document.getElementById("FunCaptcha-Token").value, u: uuid, v: vid}));
160
- var cookieParts = [
161
- pxCaptcha,
162
- "; expires=",
163
- expiryUtc,
164
- "; path=/"
165
- ];
166
-
167
- document.cookie = cookieParts.join("");
168
- location.reload();
169
- }
170
- });
147
+ window._pxAppId = '{{appId}}';
148
+ window._pxJsClientSrc = '{{{jsClientSrc}}}';
149
+ window._pxFirstPartyEnabled = {{firstPartyEnabled}};
150
+ window._pxVid = '{{vid}}';
151
+ window._pxUuid = '{{uuid}}';
152
+ window._pxHostUrl = '{{{hostUrl}}}';
153
+ </script>
154
+ <script>
155
+ var s = document.createElement('script');
156
+ s.src = '{{{blockScript}}}';
157
+ var p = document.getElementsByTagName('head')[0];
158
+ p.insertBefore(s, null);
159
+ if ({{firstPartyEnabled}}) {
160
+ s.onerror = function () {
161
+ s = document.createElement('script');
162
+ var suffixIndex = '{{{blockScript}}}'.indexOf('captcha.js');
163
+ var temperedBlockScript = '{{{blockScript}}}'.substring(suffixIndex);
164
+ s.src = '//captcha.px-cdn.net/{{appId}}/' + temperedBlockScript;
165
+ p.parentNode.insertBefore(s, p);
166
+ };
171
167
  }
172
168
  </script>
169
+
173
170
  <!-- Custom Script -->
174
- {{# jsRef }}
175
- <script src="{{ . }}"></script>
176
- {{/ jsRef }}
171
+ {{#jsRef}}
172
+ <script src="{{{jsRef}}}"></script>
173
+ {{/jsRef}}
177
174
  </body>
178
175
  </html>
@@ -0,0 +1,9 @@
1
+ <html>
2
+ <head>
3
+ <title>Too Many Requests</title>
4
+ </head>
5
+ <body>
6
+ <h1>Too Many Requests</h1>
7
+ <p>Reached maximum requests limitation, try again soon.</p>
8
+ </body>
9
+ </html>
@@ -1,3 +1,3 @@
1
1
  module PxModule
2
- VERSION = '1.4.0'
2
+ VERSION = '2.0.0'
3
3
  end
@@ -22,8 +22,8 @@ Gem::Specification.new do |gem|
22
22
  gem.bindir = "exe"
23
23
  gem.executables = gem.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
24
  gem.require_paths = ["lib"]
25
- gem.add_development_dependency "bundler", "~> 1.14"
26
- gem.add_development_dependency "rake", "~> 10.0"
25
+ gem.add_development_dependency "bundler", ">= 2.1"
26
+ gem.add_development_dependency "rake", ">= 12.3"
27
27
 
28
28
  gem.extra_rdoc_files = ["readme.md", "changelog.md"]
29
29
  gem.rdoc_options = ["--line-numbers", "--inline-source", "--title", "PerimeterX"]
@@ -33,7 +33,7 @@ Gem::Specification.new do |gem|
33
33
  gem.add_dependency('concurrent-ruby', '~> 1.0', '>= 1.0.5')
34
34
  gem.add_dependency('typhoeus', '~> 1.1', '>= 1.1.2')
35
35
  gem.add_dependency('mustache', '~> 1.0', '>= 1.0.3')
36
- gem.add_dependency('activesupport', '>= 4.2.0')
36
+ gem.add_dependency('activesupport', '>= 5.2.4.3')
37
37
 
38
38
  gem.add_development_dependency 'rspec', '~> 3.0'
39
39
  gem.add_development_dependency 'mocha', '~> 1.2', '>= 1.2.1'
data/readme.md CHANGED
@@ -29,6 +29,7 @@ Table of Contents
29
29
  * [Additional Page Activity Handler](#additional-page-activity-handler)
30
30
  * [Monitor Only](#logging)
31
31
  * [Debug Mode](#debug-mode)
32
+ * [Whitelist Routes](#whitelist-routes)
32
33
 
33
34
  **[Contributing](#contributing)**
34
35
 
@@ -87,7 +88,7 @@ All parameters are obtainable via the PerimeterX Portal. (Applications and Polic
87
88
 
88
89
  <a name="blocking-score"></a>**Changing the Minimum Score for Blocking**
89
90
 
90
- >Note: Default blocking value: 70
91
+ >Note: Default blocking value: 100
91
92
 
92
93
  ```ruby
93
94
  params = {
@@ -138,13 +139,13 @@ params = {
138
139
  ...
139
140
  :custom_verification_handler => -> (px_ctx) {
140
141
  block_score = px_ctx.context[:score];
141
- block_uuid = px_ctx.context[:uuid];
142
+ client_uuid = px_ctx.context[:uuid];
142
143
  full_url = px_ctx.context[:full_url];
143
144
 
144
145
  html = "<html>
145
146
  <body>
146
147
  <div>Access to #{full_url} has been blocked.</div>
147
- <div>Block reference - #{block_uuid} </div>
148
+ <div>Block reference - #{client_uuid} </div>
148
149
  <div>Block score - #{block_score} </div>
149
150
  </body>
150
151
  </html>".html_safe
@@ -235,12 +236,11 @@ params[:captcha_enabled] = false
235
236
 
236
237
  The CAPTCHA part of the block page can use one of the following:
237
238
  * [reCAPTCHA](https://www.google.com/recaptcha)
238
- * [FunCaptcha](https://www.funcaptcha.com/)
239
239
 
240
240
  Default: 'reCaptcha'
241
241
 
242
242
  ```ruby
243
- captchaProvider = "funCaptcha"
243
+ captchaProvider = "reCaptcha"
244
244
  ```
245
245
 
246
246
  <a name="custom-uri"></a>**Custom URI**
@@ -306,6 +306,16 @@ Enables debug logging mode to STDOUT
306
306
  params[:debug] = true
307
307
  ```
308
308
 
309
+ <a name="whitelist-routes"></a>**Whitelist Routes**
310
+ Default: []
311
+ An array of route prefixes and/or regular expressions that are always whitelisted and not validated by PerimeterX.
312
+ A string value of a path will be treated as a prefix.
313
+ A regexp value of a path will be treated as is.
314
+
315
+ ```ruby
316
+ params[:whitelist_routes] = ["/example", /\A\/example\z/]
317
+ ```
318
+
309
319
  <a name="contributing"></a># Contributing #
310
320
  ------------------------------
311
321
  The following steps are welcome when contributing to our project.