From e101026d98e650a85cba1964f9d87310fa375b9b Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Sun, 15 Dec 2024 09:34:55 -0500 Subject: [PATCH] Spam next= middleware detection and prevention. modified: make_post_sell/__init__.py modified: make_post_sell/request_methods.py modified: make_post_sell/views/__init__.py --- make_post_sell/__init__.py | 48 ++++++++++++++++++++++++++++++- make_post_sell/request_methods.py | 19 ------------ make_post_sell/views/__init__.py | 15 +++++++--- 3 files changed, 58 insertions(+), 24 deletions(-) diff --git a/make_post_sell/__init__.py b/make_post_sell/__init__.py index 4806777..5f5fb69 100644 --- a/make_post_sell/__init__.py +++ b/make_post_sell/__init__.py @@ -2,6 +2,46 @@ from pyramid.config import Configurator from pyramid.session import SignedCookieSessionFactory from pyramid.session import JSONSerializer +import re + + +class SpamBlockerMiddleware: + """ + Middleware to block spammy requests based on query string patterns. + """ + + def __init__(self, app): + self.app = app + + def __call__(self, environ, start_response): + # Extract the query string + query_string = environ.get("QUERY_STRING", "") + + # Define regex patterns + unescaped_next_count = len(re.findall(r"(?i)\bnext=", query_string)) + escaped_next_count = len( + re.findall(r"(?i)\bnext%25|next%3D|next%253D|next%2525", query_string) + ) + + # Check if the request is spammy + if unescaped_next_count + escaped_next_count > 1: + # Respond with 401 Unauthorized + print("next= query spam detected.") + status = "401 Unauthorized" + headers = [("Content-Type", "text/plain")] + start_response(status, headers) + return [b"Request appears malformed or spammy.\n"] + + # Forward the request to the next application/middleware + return self.app(environ, start_response) + + def __getattr__(self, name): + """ + Delegate attribute access to the wrapped Pyramid app. + This ensures that attributes like 'registry' are accessible. + """ + return getattr(self.app, name) + def get_int_or_bool_or_none_or_str(value): """ @@ -76,4 +116,10 @@ def main(global_config, **settings): # scan each of these includes for additional configuration. config.scan() - return config.make_wsgi_app() + # generate app. + app = config.make_wsgi_app() + + # Wrap the Pyramid app with SpamBlockerMiddleware. + app = SpamBlockerMiddleware(app) + + return app diff --git a/make_post_sell/request_methods.py b/make_post_sell/request_methods.py index f35872f..40ff94f 100644 --- a/make_post_sell/request_methods.py +++ b/make_post_sell/request_methods.py @@ -106,32 +106,13 @@ def includeme(config): def add_spam(request): """Test if request looks spammy. Returns HTTP Error or False.""" - import re from pyramid.httpexceptions import HTTPUnauthorized - query_string = request.query_string - - # Check for double or nested "next=" patterns - unescaped_next_count = len(re.findall(r"(?i)\bnext=", query_string)) - escaped_next_count = len( - re.findall(r"(?i)\bnext%25|next%3D|next%253D|next%2525", query_string) - ) - - if unescaped_next_count + escaped_next_count > 1: - print( - f"Blocked spam request from {request.remote_addr}: " - f"Multiple or nested 'next=' patterns detected." - ) - return HTTPUnauthorized("Request appears malformed or spammy.") - - # Existing hidden spam field detection if request.params.get("email2", "") != "": print( f"Blocked spam request from {request.remote_addr}: Hidden field populated." ) return HTTPUnauthorized("You smell like a spammer.") - - # If no spam detected, return False return False def add_app(request): diff --git a/make_post_sell/views/__init__.py b/make_post_sell/views/__init__.py index 5126097..229de96 100644 --- a/make_post_sell/views/__init__.py +++ b/make_post_sell/views/__init__.py @@ -11,25 +11,32 @@ def user_required( flash_msg="You must log in to access that area.", flash_level="error", redirect_to_route_name="", + max_redirects=3, # Limit the number of redirects ): """This view requires that the request has a user.""" - def wrapped(fn): def inner(request): if request.user and request.user.authenticated: + # Reset redirect count on successful authentication + request.session.pop("redirect_count", None) return fn(request) + # Track redirection attempts + redirect_count = request.session.get("redirect_count", 0) + if redirect_count >= max_redirects: + # Redirect to a safe default page if max redirects reached + request.session.flash(("Too many redirects, please try again later.", "error")) + return HTTPFound(request.route_url("home")) + # Increment redirect count + request.session["redirect_count"] = redirect_count + 1 # Flash message request.session.flash((flash_msg, flash_level)) # Redirect to the login route if redirect_to_route_name: return HTTPFound(request.route_url(redirect_to_route_name)) return HTTPFound(get_referer_or_home(request)) - return inner - return wrapped - # view decorator. def shop_is_ready_required( flash_msg="Sorry, this shop is not ready to make sales yet. Please try again later.",