From efc17974c5bbe9ea1147c3b752f1969b241215b9 Mon Sep 17 00:00:00 2001 From: Shahar Taite Date: Mon, 17 Feb 2020 13:49:11 +0200 Subject: [PATCH 1/5] dev - update console links portal sunsetting --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index deb21b6..59faf55 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,8 @@ px_config = { application = get_wsgi_application() application = PerimeterX(application, px_config) ``` -- The PerimeterX **Application ID** / **AppId** and PerimeterX **Token** / **Auth Token** can be found in the Portal, in [Applications](https://console.perimeterx.com/#/app/applicationsmgmt). -- PerimeterX **Risk Cookie** / **Cookie Key** can be found in the portal, in [Policies](https://console.perimeterx.com/#/app/policiesmgmt). +- The PerimeterX **Application ID** / **AppId** and PerimeterX **Token** / **Auth Token** can be found in the Portal, in [Applications](https://console.perimeterx.com/botDefender/admin?page=applicationsmgmt). +- PerimeterX **Risk Cookie** / **Cookie Key** can be found in the portal, in [Policies](https://console.perimeterx.com/botDefender/admin?page=policiesmgmt). The Policy from where the **Risk Cookie** / **Cookie Key** is taken must correspond with the Application from where the **Application ID** / **AppId** and PerimeterX **Token** / **Auth Token**. For details on how to create a custom Captcha page, refer to the [documentation](https://console.perimeterx.com/docs/server_integration_new.html#custom-captcha-section) From 92b0fe37e01adeb436cf40d630edc2118d72b522 Mon Sep 17 00:00:00 2001 From: SaraLumelsky <37770740+SaraLumelsky@users.noreply.github.com> Date: Sun, 3 May 2020 11:34:25 +0300 Subject: [PATCH 2/5] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 59faf55..b8cf754 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ application = PerimeterX(application, px_config) - The PerimeterX **Application ID** / **AppId** and PerimeterX **Token** / **Auth Token** can be found in the Portal, in [Applications](https://console.perimeterx.com/botDefender/admin?page=applicationsmgmt). - PerimeterX **Risk Cookie** / **Cookie Key** can be found in the portal, in [Policies](https://console.perimeterx.com/botDefender/admin?page=policiesmgmt). The Policy from where the **Risk Cookie** / **Cookie Key** is taken must correspond with the Application from where the **Application ID** / **AppId** and PerimeterX **Token** / **Auth Token**. -For details on how to create a custom Captcha page, refer to the [documentation](https://console.perimeterx.com/docs/server_integration_new.html#custom-captcha-section) +For details on how to create a custom Captcha page, refer to the [documentation](https://docs.perimeterx.com/pxconsole/docs/customize-challenge-page) ## Optional Configuration In addition to the basic installation configuration [above](#required_config), the following configurations options are available: From 77f7f74d3d1c4f31017bb25e4c9b24fd8d825c8f Mon Sep 17 00:00:00 2001 From: Johnny Tordgeman Date: Mon, 31 Aug 2020 13:49:19 +0300 Subject: [PATCH 3/5] SE-443 - monitored routes support (#10) * Added support for monitored routes * added monitored_routes tests * updated readme * updated logo * updated pylint * variable rename in px_cookie --- README.md | 22 +++++++-- dev-requirements.txt | 2 +- perimeterx/middleware.py | 7 ++- perimeterx/px_activities_client.py | 3 +- perimeterx/px_api.py | 3 +- perimeterx/px_config.py | 9 ++++ perimeterx/px_context.py | 11 ++++- perimeterx/px_cookie.py | 2 +- perimeterx/px_httpc.py | 2 +- perimeterx/px_request_verifier.py | 9 ++-- test/test_px_request_validator.py | 72 ++++++++++++++++++++++++++++++ 11 files changed, 125 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 59faf55..76a18d7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![Build Status](https://travis-ci.org/PerimeterX/perimeterx-python-3-wsgi.svg?branch=master)](https://travis-ci.org/PerimeterX/perimeterx-python-3-wsgi) [![Known Vulnerabilities](https://snyk.io/test/github/PerimeterX/perimeterx-python-3-wsgi/badge.svg)](https://snyk.io/test/github/PerimeterX/perimeterx-python-3-wsgi) -![image](https://s.perimeterx.net/logo.png) +![image](https://storage.googleapis.com/perimeterx-logos/primary_logo_red_cropped.png) [PerimeterX](http://www.perimeterx.com) Python 3 Middleware ============================================================= @@ -29,6 +29,7 @@ Table of Contents * [Px Disable Request](#px_disable_request) * [Test Block Flow on Monitoring Mode](#bypass_monitor_header) * [Enforce Specific Routes](#enforce_specific_routes) + * [Monitor Specific Routes](#monitor_specific_routes) ## Installation @@ -281,16 +282,31 @@ config = { } ``` -#### Enforced Specific Routes +#### Enforce Specific Routes An array of route prefixes that are always validated by the PerimeterX Worker (as opposed to whitelisted routes). When this property is set, any route which is not added - will be whitelisted. **Default:** Empty - ```python + +```python config = { ... enforced_specific_routes: ['/profile'] ... }; ``` + +#### Monitor Specific Routes + +An array of route prefixes that are always set to be in [monitor mode](#module_mode). This configuration is effective only when the module is enabled and in blocking mode. + +**Default:** Empty + +```python +config = { + ... + monitored_specific_routes: ['/profile'] + ... +}; +``` \ No newline at end of file diff --git a/dev-requirements.txt b/dev-requirements.txt index d7ecbc2..562385c 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,3 +1,3 @@ mock==3.0.5 requests_mock==1.7.0 -pylint==2.4.2 +pylint==2.5.0 diff --git a/perimeterx/middleware.py b/perimeterx/middleware.py index 5a74c61..ff0dd16 100644 --- a/perimeterx/middleware.py +++ b/perimeterx/middleware.py @@ -32,7 +32,6 @@ def __init__(self, app, config=None): self._config = px_config self._request_verifier = PxRequestVerifier(px_config) px_activities_client.init_activities_configuration(px_config) - px_activities_client.send_enforcer_telemetry_activity(config=px_config, update_reason='initial_config') def __call__(self, environ, start_response): px_activities_client.send_activities_in_thread() @@ -53,7 +52,7 @@ def __call__(self, environ, start_response): return verified_response(environ, pxhd_callback) except Exception as err: - self._config.logger.error("Caught exception, passing request1111. Exception: {}".format(err)) + self._config.logger.error("Caught exception, passing request. Exception: {}".format(err)) if context: self.report_pass_traffic(context) else: @@ -63,7 +62,7 @@ def __call__(self, environ, start_response): def verify(self, request): config = self.config logger = config.logger - logger.debug('Starting request verification') + logger.debug("Starting request verification {}".format(request.path)) ctx = None try: if not config._module_enabled: @@ -72,7 +71,7 @@ def verify(self, request): ctx = PxContext(request, config) return ctx, self._request_verifier.verify_request(ctx, request) except Exception as err: - logger.error("Caught exception222, passing request. Exception: {}".format(err)) + logger.error("Caught exception in verify, passing request. Exception: {}".format(err)) if ctx: self.report_pass_traffic(ctx) else: diff --git a/perimeterx/px_activities_client.py b/perimeterx/px_activities_client.py index 5e464b9..e865699 100644 --- a/perimeterx/px_activities_client.py +++ b/perimeterx/px_activities_client.py @@ -47,7 +47,6 @@ def send_to_perimeterx(activity_type, ctx, config, detail): 'http_method': ctx.http_method, 'http_version': ctx.http_version, 'module_version': config.module_version, - 'risk_mode': config.module_mode, } if len(detail.keys()) > 0: @@ -84,7 +83,7 @@ def send_block_activity(ctx, config): 'risk_rtt': ctx.risk_rtt, 'cookie_origin': ctx.cookie_origin, 'block_action': ctx.block_action, - 'simulated_block': config.module_mode is px_constants.MODULE_MODE_MONITORING, + 'simulated_block': config.module_mode is px_constants.MODULE_MODE_MONITORING or ctx.monitored_route, }) diff --git a/perimeterx/px_api.py b/perimeterx/px_api.py index 10b30ea..f10da0a 100644 --- a/perimeterx/px_api.py +++ b/perimeterx/px_api.py @@ -112,6 +112,7 @@ def verify(ctx, config): def prepare_risk_body(ctx, config): logger = config.logger + risk_mode = 'monitor' if config.module_mode == px_constants.MODULE_MODE_MONITORING or ctx.monitored_route else 'active_blocking' body = { 'request': { 'ip': ctx.ip, @@ -125,7 +126,7 @@ def prepare_risk_body(ctx, config): 'http_method': ctx.http_method, 'http_version': ctx.http_version, 'module_version': config.module_version, - 'risk_mode': config.module_mode, + 'risk_mode': risk_mode, 'cookie_origin': ctx.cookie_origin } } diff --git a/perimeterx/px_config.py b/perimeterx/px_config.py index f848cb5..fdf6e46 100644 --- a/perimeterx/px_config.py +++ b/perimeterx/px_config.py @@ -54,6 +54,11 @@ def __init__(self, config_dict): raise TypeError('enforced_specific_routes must be a list') self._enforced_specific_routes = enforced_routes + monitored_routes = config_dict.get('monitored_specific_routes', []) + if not isinstance(monitored_routes, list): + raise TypeError('monitored_specific_routes must be a list') + self._monitored_specific_routes = monitored_routes + self._block_html = 'BLOCK' self._logo_visibility = 'visible' if custom_logo is not None else 'hidden' self._telemetry_config = self.__create_telemetry_config() @@ -205,6 +210,10 @@ def bypass_monitor_header(self): def enforced_specific_routes(self): return self._enforced_specific_routes + @property + def monitored_specific_routes(self): + return self._monitored_specific_routes + def __instantiate_user_defined_handlers(self, config_dict): self._custom_request_handler = self.__set_handler('custom_request_handler', config_dict) self._get_user_ip = self.__set_handler('get_user_ip', config_dict) diff --git a/perimeterx/px_context.py b/perimeterx/px_context.py index acb7d96..129b335 100644 --- a/perimeterx/px_context.py +++ b/perimeterx/px_context.py @@ -52,7 +52,7 @@ def __init__(self, request, config): sensitive_route = sum(1 for _ in filter(lambda sensitive_route_item: uri.startswith(sensitive_route_item), config.sensitive_routes)) > 0 whitelist_route = sum(1 for _ in filter(lambda whitelist_route_item: uri.startswith(whitelist_route_item), config.whitelist_routes)) > 0 enforced_route = sum(1 for _ in filter(lambda enforced_route_item: uri.startswith(enforced_route_item), config.enforced_specific_routes)) > 0 - + monitored_route = not enforced_route and sum(1 for _ in filter(lambda monitored_route_item: uri.startswith(monitored_route_item), config.monitored_specific_routes)) > 0 protocol_split = request.environ.get('SERVER_PROTOCOL', '').split('/') if protocol_split[0].startswith('HTTP'): self._http_protocol = protocol_split[0].lower() + '://' @@ -78,6 +78,7 @@ def __init__(self, request, config): self._sensitive_route = sensitive_route self._whitelist_route = whitelist_route self._enforced_route = enforced_route + self._monitored_route = monitored_route self._s2s_call_reason = 'none' self._cookie_origin = cookie_origin self._is_mobile = cookie_origin == "header" @@ -259,6 +260,14 @@ def whitelist_route(self): def whitelist_route(self, whitelist_route): self._whitelist_route = whitelist_route + @property + def monitored_route(self): + return self._monitored_route + + @monitored_route.setter + def monitored_route(self, monitored_route): + self._monitored_route = monitored_route + @property def s2s_call_reason(self): return self._s2s_call_reason diff --git a/perimeterx/px_cookie.py b/perimeterx/px_cookie.py index d8a13b3..ec9ba70 100644 --- a/perimeterx/px_cookie.py +++ b/perimeterx/px_cookie.py @@ -64,7 +64,7 @@ def decrypt_cookie(self): if iterations < 1 or iterations > 10000: return False data = base64.b64decode(parts[2]) - dk = hashlib.pbkdf2_hmac(hash_name='sha256', password=config.cookie_key.encode(), salt=salt, iterations=iterations, dklen=48) + dk = hashlib.pbkdf2_hmac(hash_name='sha256', password=self._config.cookie_key.encode(), salt=salt, iterations=iterations, dklen=48) key = dk[:32] iv = dk[32:] cipher = AES.new(key, AES.MODE_CBC, iv) diff --git a/perimeterx/px_httpc.py b/perimeterx/px_httpc.py index 87b4fd5..046f212 100644 --- a/perimeterx/px_httpc.py +++ b/perimeterx/px_httpc.py @@ -28,7 +28,7 @@ def send(full_url, body, headers, config, method, raise_error = False): response = requests.post(url='https://' + full_url, headers=headers, data=body, timeout=config.api_timeout) if response.status_code >= 400: - logger.debug('PerimeterX server call failed') + logger.debug('PerimeterX server call failed: ' + str(response.status_code)) return False finish = time.time() request_time = finish - start diff --git a/perimeterx/px_request_verifier.py b/perimeterx/px_request_verifier.py index b6802da..ddadd0a 100644 --- a/perimeterx/px_request_verifier.py +++ b/perimeterx/px_request_verifier.py @@ -24,6 +24,9 @@ def verify_request(self, ctx, request): if px_utils.is_static_file(ctx): self.logger.debug('Filter static file request. uri: {}'.format(uri)) return True + if request.environ.get('REQUEST_METHOD') == 'OPTIONS': + self.logger.debug('The request method was whitelisted, OPTIONS') + return True if ctx.whitelist_route: self.logger.debug('The requested uri is whitelisted, passing request') return True @@ -56,11 +59,11 @@ def handle_verification(self, ctx, request): should_bypass_monitor = config.bypass_monitor_header and ctx.headers.get(config.bypass_monitor_header) == '1'; if config.additional_activity_handler: config.additional_activity_handler(ctx, config) - if config.module_mode == px_constants.MODULE_MODE_BLOCKING or should_bypass_monitor: + if ctx.monitored_route or (config.module_mode == px_constants.MODULE_MODE_MONITORING and not should_bypass_monitor): + return True + else: data, headers, status = self.px_blocker.handle_blocking(ctx=ctx, config=config) response_function = generate_blocking_response(data, headers, status) - else: - pass_request = True if config.custom_request_handler: data, headers, status = config.custom_request_handler(ctx, self.config, request) diff --git a/test/test_px_request_validator.py b/test/test_px_request_validator.py index 45e4605..60e1e89 100644 --- a/test/test_px_request_validator.py +++ b/test/test_px_request_validator.py @@ -196,3 +196,75 @@ def test_specific_enforced_routes_with_unenforced_route(self): context.score = 100 response = request_handler.verify_request(context, request) self.assertEqual(response, True) + + def test_monitor_specific_routes_in_blocking_mode(self): + config = PxConfig({'app_id': 'PXfake_app_id', + 'auth_token': '', + 'module_mode': px_constants.MODULE_MODE_BLOCKING, + 'monitored_specific_routes': ['/profile'], + }); + headers = {'X-FORWARDED-FOR': '127.0.0.1', + 'remote-addr': '127.0.0.1', + 'content_length': '100'} + request_handler = PxRequestVerifier(config) + builder = EnvironBuilder(headers=headers, path='/profile') + env = builder.get_environ() + request = Request(env) + context = PxContext(request, request_handler.config) + response = request_handler.verify_request(context, request) + self.assertEqual(context.monitored_route, True) + + def test_monitor_specific_routes_in_blocking_mode_should_block_other_routes(self): + config = PxConfig({'app_id': 'PXfake_app_id', + 'auth_token': '', + 'module_mode': px_constants.MODULE_MODE_BLOCKING, + 'monitored_specific_routes': ['/profile'], + }); + headers = {'X-FORWARDED-FOR': '127.0.0.1', + 'remote-addr': '127.0.0.1', + 'content_length': '100'} + request_handler = PxRequestVerifier(config) + builder = EnvironBuilder(headers=headers, path='/') + env = builder.get_environ() + request = Request(env) + context = PxContext(request, request_handler.config) + context.score = 100 + response = request_handler.handle_verification(context, request) + self.assertEqual(response.status, '403 Forbidden') + + def test_enforced_specific_routes_overrides_monitor_specific_routes(self): + config = PxConfig({'app_id': 'PXfake_app_id', + 'auth_token': '', + 'module_mode': px_constants.MODULE_MODE_BLOCKING, + 'monitored_specific_routes': ['/profile'], + 'enforced_specific_routes': ['/profile', '/login'], + }); + headers = {'X-FORWARDED-FOR': '127.0.0.1', + 'remote-addr': '127.0.0.1', + 'content_length': '100'} + request_handler = PxRequestVerifier(config) + builder = EnvironBuilder(headers=headers, path='/profile') + env = builder.get_environ() + request = Request(env) + context = PxContext(request, request_handler.config) + context.score = 100 + response = request_handler.handle_verification(context, request) + self.assertEqual(response.status, '403 Forbidden') + + def test_monitor_specific_routes_with_enforced_specific_routes(self): + config = PxConfig({'app_id': 'PXfake_app_id', + 'auth_token': '', + 'module_mode': px_constants.MODULE_MODE_BLOCKING, + 'monitored_specific_routes': ['/profile'], + 'enforced_specific_routes': ['/login'], + }); + headers = {'X-FORWARDED-FOR': '127.0.0.1', + 'remote-addr': '127.0.0.1', + 'content_length': '100'} + request_handler = PxRequestVerifier(config) + builder = EnvironBuilder(headers=headers, path='/profile') + env = builder.get_environ() + request = Request(env) + context = PxContext(request, request_handler.config) + response = request_handler.verify_request(context, request) + self.assertEqual(context.monitored_route, True) \ No newline at end of file From 4018456ba1b5cc68d828f4246e6709bff13ed39f Mon Sep 17 00:00:00 2001 From: Johnny Tordgeman Date: Tue, 1 Sep 2020 10:04:10 +0300 Subject: [PATCH 4/5] added regex support --- README.md | 124 ++++++++++++++++++++++-------- perimeterx/px_config.py | 36 +++++++++ perimeterx/px_context.py | 12 +-- test/test_px_request_validator.py | 108 ++++++++++++++++++++++++++ 4 files changed, 244 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index db06d1d..b1d674a 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,13 @@ Table of Contents * [Send Page Activities](#send_page_activities) * [Debug Mode](#debug_mode) * [Sensitive Routes](#sensitive_routes) + * [Sensitive Routes Regex](#sensitive_routes_regex) * [Whitelist Routes](#whitelist_routes) + * [Whitelist Routes Regex](#whitelist_routes_regex) + * [Enforce Specific Routes](#enforce_specific_routes) + * [Enforce Specific Routes Regex](#enforce_specific_routes_regex) + * [Monitor Specific Routes](#monitor_specific_routes) + * [Monitor Specific Routes Regex](#monitor_specific_routes_regex) * [Sensitive Headers](#sensitive_headers) * [IP Headers](#ip_headers) * [First-Party Enabled](#first_party_enabled) @@ -28,8 +34,6 @@ Table of Contents * [Additional Activity Handler](#additional_activity_handler) * [Px Disable Request](#px_disable_request) * [Test Block Flow on Monitoring Mode](#bypass_monitor_header) - * [Enforce Specific Routes](#enforce_specific_routes) - * [Monitor Specific Routes](#monitor_specific_routes) ## Installation @@ -153,6 +157,21 @@ config = { ... } ``` + +#### Sensitive Routes Regex + +An array of regex patterns that trigger a server call to PerimeterX servers every time the page is viewed, regardless of viewing history. + +**Default:** Empty + +```python +config = { + ... + sensitive_routes_regex: [r'^/login$', r'^/user'] + ... +} +``` + #### Whitelist Routes An array of route prefixes which will bypass enforcement (will never get scored). @@ -167,6 +186,78 @@ config = { } ``` +#### Whitelist Routes Regex + +An array of regex patterns which will bypass enforcement (will never get scored). + +**Default:** Empty + +```python +config = { + ... + whitelist_routes_regex: [r'^/about'] + ... +} +``` + +#### Enforce Specific Routes + +An array of route prefixes that are always validated by the PerimeterX Worker (as opposed to whitelisted routes). +When this property is set, any route which is not added - will be whitelisted. + +**Default:** Empty + +```python +config = { + ... + enforced_specific_routes: ['/profile'] + ... +}; +``` + +#### Enforce Specific Routes Regex + +An array of regex patterns that are always validated by the PerimeterX Worker (as opposed to whitelisted routes). +When this property is set, any route which is not added - will be whitelisted. + +**Default:** Empty + +```python +config = { + ... + enforced_specific_routes_regex: [r'^/profile$'] + ... +}; +``` + +#### Monitor Specific Routes + +An array of route prefixes that are always set to be in [monitor mode](#module_mode). This configuration is effective only when the module is enabled and in blocking mode. + +**Default:** Empty + +```python +config = { + ... + monitored_specific_routes: ['/profile'] + ... +}; +``` + +#### Monitor Specific Routes Regex + +An array of regex patterns that are always set to be in [monitor mode](#module_mode). This configuration is effective only when the module is enabled and in blocking mode. + +**Default:** Empty + +```python +config = { + ... + monitored_specific_routes_regex: [r'^/profile/me$'] + ... +}; +``` + #### Sensitive Headers An array of headers that are not sent to PerimeterX servers on API calls. @@ -280,33 +371,4 @@ config = { bypass_monitor_header: 'x-px-block', ... } -``` - -#### Enforce Specific Routes - -An array of route prefixes that are always validated by the PerimeterX Worker (as opposed to whitelisted routes). -When this property is set, any route which is not added - will be whitelisted. - -**Default:** Empty - -```python -config = { - ... - enforced_specific_routes: ['/profile'] - ... -}; -``` - -#### Monitor Specific Routes - -An array of route prefixes that are always set to be in [monitor mode](#module_mode). This configuration is effective only when the module is enabled and in blocking mode. - -**Default:** Empty - -```python -config = { - ... - monitored_specific_routes: ['/profile'] - ... -}; ``` \ No newline at end of file diff --git a/perimeterx/px_config.py b/perimeterx/px_config.py index fdf6e46..9d6453b 100644 --- a/perimeterx/px_config.py +++ b/perimeterx/px_config.py @@ -59,6 +59,26 @@ def __init__(self, config_dict): raise TypeError('monitored_specific_routes must be a list') self._monitored_specific_routes = monitored_routes + sensitive_routes_regex = config_dict.get('sensitive_routes_regex', []) + if not isinstance(sensitive_routes_regex, list): + raise TypeError('sensitive_routes_regex must be a list') + self._sensitive_routes_regex = sensitive_routes_regex + + whitelist_routes_regex = config_dict.get('whitelist_routes_regex', []) + if not isinstance(whitelist_routes_regex, list): + raise TypeError('whitelist_routes_regex must be a list') + self._whitelist_routes_regex = whitelist_routes_regex + + enforced_routes_regex = config_dict.get('enforced_specific_routes_regex', []) + if not isinstance(enforced_routes_regex, list): + raise TypeError('enforced_specific_routes must be a list') + self._enforced_specific_routes_regex = enforced_routes_regex + + monitored_routes_regex = config_dict.get('monitored_specific_routes_regex', []) + if not isinstance(monitored_routes_regex, list): + raise TypeError('monitored_specific_routes_regex must be a list') + self._monitored_specific_routes_regex = monitored_routes_regex + self._block_html = 'BLOCK' self._logo_visibility = 'visible' if custom_logo is not None else 'hidden' self._telemetry_config = self.__create_telemetry_config() @@ -170,6 +190,14 @@ def sensitive_routes(self): def whitelist_routes(self): return self._whitelist_routes + @property + def sensitive_routes_regex(self): + return self._sensitive_routes_regex + + @property + def whitelist_routes_regex(self): + return self._whitelist_routes_regex + @property def block_html(self): return self._block_html @@ -214,6 +242,14 @@ def enforced_specific_routes(self): def monitored_specific_routes(self): return self._monitored_specific_routes + @property + def enforced_specific_routes_regex(self): + return self._enforced_specific_routes_regex + + @property + def monitored_specific_routes_regex(self): + return self._monitored_specific_routes_regex + def __instantiate_user_defined_handlers(self, config_dict): self._custom_request_handler = self.__set_handler('custom_request_handler', config_dict) self._get_user_ip = self.__set_handler('get_user_ip', config_dict) diff --git a/perimeterx/px_context.py b/perimeterx/px_context.py index 129b335..1bd4728 100644 --- a/perimeterx/px_context.py +++ b/perimeterx/px_context.py @@ -1,9 +1,10 @@ +import re + from requests.structures import CaseInsensitiveDict from perimeterx.px_constants import * from perimeterx.px_data_enrichment_cookie import PxDataEnrichmentCookie - class PxContext(object): def __init__(self, request, config): @@ -49,10 +50,11 @@ def __init__(self, request, config): uri = request.path full_url = request.url hostname = request.host - sensitive_route = sum(1 for _ in filter(lambda sensitive_route_item: uri.startswith(sensitive_route_item), config.sensitive_routes)) > 0 - whitelist_route = sum(1 for _ in filter(lambda whitelist_route_item: uri.startswith(whitelist_route_item), config.whitelist_routes)) > 0 - enforced_route = sum(1 for _ in filter(lambda enforced_route_item: uri.startswith(enforced_route_item), config.enforced_specific_routes)) > 0 - monitored_route = not enforced_route and sum(1 for _ in filter(lambda monitored_route_item: uri.startswith(monitored_route_item), config.monitored_specific_routes)) > 0 + sensitive_route = sum(1 for _ in filter(lambda sensitive_route_item: re.search(sensitive_route_item, uri), config.sensitive_routes_regex)) > 0 or sum(1 for _ in filter(lambda sensitive_route_item: uri.startswith(sensitive_route_item), config.sensitive_routes)) > 0 + whitelist_route = sum(1 for _ in filter(lambda whitelist_route_item: re.search(whitelist_route_item, uri), config.whitelist_routes_regex)) > 0 or sum(1 for _ in filter(lambda whitelist_route_item: uri.startswith(whitelist_route_item), config.whitelist_routes)) > 0 + enforced_route = sum(1 for _ in filter(lambda enforced_route_item: re.search(enforced_route_item, uri), config.enforced_specific_routes_regex)) > 0 or sum(1 for _ in filter(lambda enforced_route_item: uri.startswith(enforced_route_item), config.enforced_specific_routes)) > 0 + monitored_route = not enforced_route and (sum(1 for _ in filter(lambda monitored_route_item: re.search(monitored_route_item, uri), config.monitored_specific_routes_regex)) > 0 or sum(1 for _ in filter(lambda monitored_route_item: uri.startswith(monitored_route_item), config.monitored_specific_routes)) > 0) + protocol_split = request.environ.get('SERVER_PROTOCOL', '').split('/') if protocol_split[0].startswith('HTTP'): self._http_protocol = protocol_split[0].lower() + '://' diff --git a/test/test_px_request_validator.py b/test/test_px_request_validator.py index 60e1e89..82610e8 100644 --- a/test/test_px_request_validator.py +++ b/test/test_px_request_validator.py @@ -179,6 +179,24 @@ def test_specific_enforced_routes_with_enforced_route(self): response = request_handler.handle_verification(context, request) self.assertEqual(response.status, '403 Forbidden') + def test_specific_enforced_routes_with_enforced_route_regex(self): + config = PxConfig({'app_id': 'PXfake_app_id', + 'auth_token': '', + 'module_mode': px_constants.MODULE_MODE_BLOCKING, + 'enforced_specific_routes_regex': [r'^/profile$'], + }); + headers = {'X-FORWARDED-FOR': '127.0.0.1', + 'remote-addr': '127.0.0.1', + 'content_length': '100'} + request_handler = PxRequestVerifier(config) + builder = EnvironBuilder(headers=headers, path='/profile') + env = builder.get_environ() + request = Request(env) + context = PxContext(request, request_handler.config) + context.score = 100 + response = request_handler.handle_verification(context, request) + self.assertEqual(response.status, '403 Forbidden') + def test_specific_enforced_routes_with_unenforced_route(self): config = PxConfig({'app_id': 'PXfake_app_id', 'auth_token': '', @@ -197,6 +215,24 @@ def test_specific_enforced_routes_with_unenforced_route(self): response = request_handler.verify_request(context, request) self.assertEqual(response, True) + def test_specific_enforced_routes_with_unenforced_route_regex(self): + config = PxConfig({'app_id': 'PXfake_app_id', + 'auth_token': '', + 'module_mode': px_constants.MODULE_MODE_BLOCKING, + 'enforced_specific_routes_regex': [r'^/profile$'], + }); + headers = {'X-FORWARDED-FOR': '127.0.0.1', + 'remote-addr': '127.0.0.1', + 'content_length': '100'} + request_handler = PxRequestVerifier(config) + builder = EnvironBuilder(headers=headers, path='/') + env = builder.get_environ() + request = Request(env) + context = PxContext(request, request_handler.config) + context.score = 100 + response = request_handler.verify_request(context, request) + self.assertEqual(response, True) + def test_monitor_specific_routes_in_blocking_mode(self): config = PxConfig({'app_id': 'PXfake_app_id', 'auth_token': '', @@ -214,6 +250,23 @@ def test_monitor_specific_routes_in_blocking_mode(self): response = request_handler.verify_request(context, request) self.assertEqual(context.monitored_route, True) + def test_monitor_specific_routes_in_blocking_mode_regex(self): + config = PxConfig({'app_id': 'PXfake_app_id', + 'auth_token': '', + 'module_mode': px_constants.MODULE_MODE_BLOCKING, + 'monitored_specific_routes_regex': [r'^/profile$'], + }); + headers = {'X-FORWARDED-FOR': '127.0.0.1', + 'remote-addr': '127.0.0.1', + 'content_length': '100'} + request_handler = PxRequestVerifier(config) + builder = EnvironBuilder(headers=headers, path='/profile') + env = builder.get_environ() + request = Request(env) + context = PxContext(request, request_handler.config) + response = request_handler.verify_request(context, request) + self.assertEqual(context.monitored_route, True) + def test_monitor_specific_routes_in_blocking_mode_should_block_other_routes(self): config = PxConfig({'app_id': 'PXfake_app_id', 'auth_token': '', @@ -232,6 +285,24 @@ def test_monitor_specific_routes_in_blocking_mode_should_block_other_routes(self response = request_handler.handle_verification(context, request) self.assertEqual(response.status, '403 Forbidden') + def test_monitor_specific_routes_in_blocking_mode_should_block_other_routes_regex(self): + config = PxConfig({'app_id': 'PXfake_app_id', + 'auth_token': '', + 'module_mode': px_constants.MODULE_MODE_BLOCKING, + 'monitored_specific_routes_regex': [r'^/profile$'], + }); + headers = {'X-FORWARDED-FOR': '127.0.0.1', + 'remote-addr': '127.0.0.1', + 'content_length': '100'} + request_handler = PxRequestVerifier(config) + builder = EnvironBuilder(headers=headers, path='/profile/me') + env = builder.get_environ() + request = Request(env) + context = PxContext(request, request_handler.config) + context.score = 100 + response = request_handler.handle_verification(context, request) + self.assertEqual(response.status, '403 Forbidden') + def test_enforced_specific_routes_overrides_monitor_specific_routes(self): config = PxConfig({'app_id': 'PXfake_app_id', 'auth_token': '', @@ -251,6 +322,25 @@ def test_enforced_specific_routes_overrides_monitor_specific_routes(self): response = request_handler.handle_verification(context, request) self.assertEqual(response.status, '403 Forbidden') + def test_enforced_specific_routes_overrides_monitor_specific_routes(self): + config = PxConfig({'app_id': 'PXfake_app_id', + 'auth_token': '', + 'module_mode': px_constants.MODULE_MODE_BLOCKING, + 'monitored_specific_routes_regex': [r'^/profile$'], + 'enforced_specific_routes_regex': [r'^/profile$', r'^/login$'], + }); + headers = {'X-FORWARDED-FOR': '127.0.0.1', + 'remote-addr': '127.0.0.1', + 'content_length': '100'} + request_handler = PxRequestVerifier(config) + builder = EnvironBuilder(headers=headers, path='/profile') + env = builder.get_environ() + request = Request(env) + context = PxContext(request, request_handler.config) + context.score = 100 + response = request_handler.handle_verification(context, request) + self.assertEqual(response.status, '403 Forbidden') + def test_monitor_specific_routes_with_enforced_specific_routes(self): config = PxConfig({'app_id': 'PXfake_app_id', 'auth_token': '', @@ -267,4 +357,22 @@ def test_monitor_specific_routes_with_enforced_specific_routes(self): request = Request(env) context = PxContext(request, request_handler.config) response = request_handler.verify_request(context, request) + self.assertEqual(context.monitored_route, True) + + def test_monitor_specific_routes_with_enforced_specific_routes_regex(self): + config = PxConfig({'app_id': 'PXfake_app_id', + 'auth_token': '', + 'module_mode': px_constants.MODULE_MODE_BLOCKING, + 'monitored_specific_routes_regex': [r'^/profile$'], + 'enforced_specific_routes_regex': [r'^/login$'], + }); + headers = {'X-FORWARDED-FOR': '127.0.0.1', + 'remote-addr': '127.0.0.1', + 'content_length': '100'} + request_handler = PxRequestVerifier(config) + builder = EnvironBuilder(headers=headers, path='/profile') + env = builder.get_environ() + request = Request(env) + context = PxContext(request, request_handler.config) + response = request_handler.verify_request(context, request) self.assertEqual(context.monitored_route, True) \ No newline at end of file From f517c823f6b64c7af78c40a83813e67720b867cd Mon Sep 17 00:00:00 2001 From: Johnny Tordgeman Date: Wed, 2 Sep 2020 09:26:17 +0300 Subject: [PATCH 5/5] Version 1.1.0 --- CHANGELOG.md | 5 +++++ README.md | 2 +- perimeterx/px_constants.py | 2 +- setup.py | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4df0cf2..2b70678 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [1.1.0] - 2020-09-02 +### Added +- Support for `monitored_specific_routes` +- Support for Regex patters in sensitive/whitelist/monitored/enforced routes + ## [1.0.1] - 2019-11-10 ### Fixed - Using hashlib pbkdf2 implementation. diff --git a/README.md b/README.md index b1d674a..2daa252 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [PerimeterX](http://www.perimeterx.com) Python 3 Middleware ============================================================= -> Latest stable version: [v1.0.1](https://pypi.org/project/perimeterx-python-3-wsgi/) +> Latest stable version: [v1.1.0](https://pypi.org/project/perimeterx-python-3-wsgi/) Table of Contents ----------------- diff --git a/perimeterx/px_constants.py b/perimeterx/px_constants.py index 1de359b..50cfb67 100644 --- a/perimeterx/px_constants.py +++ b/perimeterx/px_constants.py @@ -27,7 +27,7 @@ EMPTY_GIF_B64 = 'R0lGODlhAQABAPAAAAAAAAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==' COLLECTOR_HOST = 'collector.perimeterx.net' FIRST_PARTY_FORWARDED_FOR = 'X-FORWARDED-FOR' -MODULE_VERSION = 'Python 3 WSGI Module v1.0.1' +MODULE_VERSION = 'Python 3 WSGI Module v1.1.0' API_RISK = '/api/v3/risk' PAGE_REQUESTED_ACTIVITY = 'page_requested' BLOCK_ACTIVITY = 'block' diff --git a/setup.py b/setup.py index e0db26a..8d900fb 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages -version = 'v1.0.1' +version = 'v1.1.0' setup(name='perimeterx-python-3-wsgi', version=version, license='MIT',