perimeter_x 1.4.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.