aiwaf 0.1.9.3.0__py3-none-any.whl → 0.1.9.3.1__py3-none-any.whl

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.

Potentially problematic release.


This version of aiwaf might be problematic. Click here for more details.

aiwaf/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  default_app_config = "aiwaf.apps.AiwafConfig"
2
2
 
3
- __version__ = "0.1.9.3.0"
3
+ __version__ = "0.1.9.3.1"
4
4
 
5
5
  # Note: Middleware classes are available from aiwaf.middleware
6
6
  # Import them only when needed to avoid circular imports during Django app loading
@@ -10,7 +10,8 @@ def _sort(items, order):
10
10
  reverse=reverse)
11
11
 
12
12
  def _filter_since(items, seconds):
13
- if not seconds: return items
13
+ if not seconds:
14
+ return items
14
15
  cutoff = timezone.now() - timedelta(seconds=seconds)
15
16
  return [it for it in items if it.get("created_at") and it["created_at"] >= cutoff]
16
17
 
@@ -25,57 +26,79 @@ def _print_table(rows, headers):
25
26
  print(" | ".join(str(cell).ljust(widths[i]) for i, cell in enumerate(r)))
26
27
 
27
28
  class Command(BaseCommand):
28
- help = "Lister les données AIWAF (IPs bloquées, exemptions, mots-clés dynamiques)."
29
+ help = "AIWAF list (v2): blocked IPs, exempted IPs, monitored keywords."
29
30
 
30
31
  def add_arguments(self, parser):
31
32
  grp = parser.add_mutually_exclusive_group()
32
- grp.add_argument("--ips", action="store_true", help="Lister les IPs bloquées (défaut).")
33
- grp.add_argument("--exemptions", action="store_true", help="Lister les IPs exemptées.")
34
- grp.add_argument("--keywords", action="store_true", help="Lister les mots-clés dynamiques.")
35
- grp.add_argument("--all", action="store_true", help="Tout lister.")
36
- parser.add_argument("--format", choices=["table", "json"], default="table")
37
- parser.add_argument("--limit", type=int, default=100)
38
- parser.add_argument("--order", choices=["newest", "oldest"], default="newest")
39
- parser.add_argument("--since", type=int, help="Fenêtre en secondes (ex: 86400 = 24h).")
33
+ grp.add_argument("--ips-blocked", action="store_true", help="List blocked IPs (blacklist).")
34
+ grp.add_argument("--ips-exempted", action="store_true", help="List exempted IPs (whitelist).")
35
+ grp.add_argument("--keywords-monitored", action="store_true", help="List monitored dynamic keywords.")
36
+ grp.add_argument("--all", action="store_true", help="List everything.")
37
+
38
+ parser.add_argument("--format", choices=["table", "json"], default="table", help="Output format.")
39
+ parser.add_argument("--limit", type=int, default=100, help="Max items to display.")
40
+ parser.add_argument("--order", choices=["newest", "oldest"], default="newest", help="Sort order for IP entries.")
41
+ parser.add_argument("--since", type=int, help="Time window in seconds (e.g. 86400 = last 24h) for IP entries.")
42
+ parser.add_argument("--only-ip", action="store_true", help="For IP lists: print only the IP column.")
43
+
44
+ def handle(self, *args, **options):
45
+ # default: show blocked IPs if nothing specific is requested
46
+ if not any([options["ips_blocked"], options["ips_exempted"], options["keywords_monitored"], options["all"]]):
47
+ options["ips_blocked"] = True
40
48
 
41
- def handle(self, *args, **o):
42
- if not any([o["exemptions"], o["keywords"], o["all"]]): # défaut = ips
43
- o["ips"] = True
44
49
  payload = {}
45
50
 
46
- if o["all"] or o["ips"]:
47
- data = get_blacklist_store().get_all()
48
- data = _filter_since(data, o.get("since"))
49
- data = _sort(data, o["order"])[:o["limit"]]
50
- payload["ips"] = data
51
+ if options["all"] or options["ips_blocked"]:
52
+ data = get_blacklist_store().get_all() # [{ip_address, reason, created_at}]
53
+ data = _filter_since(data, options.get("since"))
54
+ data = _sort(data, options["order"])[: options["limit"]]
55
+ payload["ips_blocked"] = data
51
56
 
52
- if o["all"] or o["exemptions"]:
53
- data = get_exemption_store().get_all()
54
- data = _filter_since(data, o.get("since"))
55
- data = _sort(data, o["order"])[:o["limit"]]
56
- payload["exemptions"] = data
57
+ if options["all"] or options["ips_exempted"]:
58
+ data = get_exemption_store().get_all() # [{ip_address, reason, created_at}]
59
+ data = _filter_since(data, options.get("since"))
60
+ data = _sort(data, options["order"])[: options["limit"]]
61
+ payload["ips_exempted"] = data
57
62
 
58
- if o["all"] or o["keywords"]:
59
- kws = get_keyword_store().get_top_keywords(o["limit"])
60
- payload["keywords"] = [{"keyword": k} for k in kws]
63
+ if options["all"] or options["keywords_monitored"]:
64
+ kws = get_keyword_store().get_top_keywords(options["limit"]) # [str]
65
+ payload["keywords_monitored"] = [{"keyword": k} for k in kws]
61
66
 
62
- if o["format"] == "json":
67
+ if options["format"] == "json":
63
68
  def _default(v):
64
- try: return v.isoformat()
65
- except Exception: return str(v)
69
+ try:
70
+ return v.isoformat()
71
+ except Exception:
72
+ return str(v)
66
73
  self.stdout.write(json.dumps(payload, ensure_ascii=False, indent=2, default=_default))
67
- else:
68
- if "ips" in payload:
69
- print("\n== IPs bloquées ==")
70
- rows = [[r.get("ip_address",""), r.get("reason",""), r.get("created_at","")]
71
- for r in payload["ips"]]
72
- _print_table(rows, ["ip_address", "reason", "created_at"])
73
- if "exemptions" in payload:
74
- print("\n== Exemptions ==")
75
- rows = [[r.get("ip_address",""), r.get("reason",""), r.get("created_at","")]
76
- for r in payload["exemptions"]]
77
- _print_table(rows, ["ip_address", "reason", "created_at"])
78
- if "keywords" in payload:
79
- print("\n== Mots-clés dynamiques ==")
80
- rows = [[r["keyword"]] for r in payload["keywords"]]
81
- _print_table(rows, ["keyword"])
74
+ return
75
+
76
+ # table output
77
+ if "ips_blocked" in payload:
78
+ print("\n== IPs blocked ==")
79
+ rows = payload["ips_blocked"]
80
+ if options["only_ip"]:
81
+ for r in rows:
82
+ print(r.get("ip_address", ""))
83
+ else:
84
+ _print_table(
85
+ [[r.get("ip_address",""), r.get("reason",""), r.get("created_at","")] for r in rows],
86
+ ["ip_address", "reason", "created_at"],
87
+ )
88
+
89
+ if "ips_exempted" in payload:
90
+ print("\n== IPs exempted ==")
91
+ rows = payload["ips_exempted"]
92
+ if options["only_ip"]:
93
+ for r in rows:
94
+ print(r.get("ip_address", ""))
95
+ else:
96
+ _print_table(
97
+ [[r.get("ip_address",""), r.get("reason",""), r.get("created_at","")] for r in rows],
98
+ ["ip_address", "reason", "created_at"],
99
+ )
100
+
101
+ if "keywords_monitored" in payload:
102
+ print("\n== Keywords monitored ==")
103
+ rows = payload["keywords_monitored"]
104
+ _print_table([[r["keyword"]] for r in rows], ["keyword"])
aiwaf/middleware.py CHANGED
@@ -4,17 +4,30 @@ import time
4
4
  import re
5
5
  import os
6
6
  import warnings
7
- import numpy as np
8
- import joblib
9
- from django.db.models import UUIDField
10
7
  from collections import defaultdict
11
8
  from django.utils.deprecation import MiddlewareMixin
12
9
  from django.http import JsonResponse
13
10
  from django.conf import settings
14
11
  from django.core.cache import cache
15
- from django.db.models import F
12
+ from django.db.models import F, UUIDField
16
13
  from django.apps import apps
17
14
  from django.urls import get_resolver
15
+
16
+ # Optional dependencies with graceful fallbacks
17
+ try:
18
+ import numpy as np
19
+ NUMPY_AVAILABLE = True
20
+ except ImportError:
21
+ np = None
22
+ NUMPY_AVAILABLE = False
23
+
24
+ try:
25
+ import joblib
26
+ JOBLIB_AVAILABLE = True
27
+ except ImportError:
28
+ joblib = None
29
+ JOBLIB_AVAILABLE = False
30
+
18
31
  from .trainer import STATIC_KW, STATUS_IDX, path_exists_in_django
19
32
  from .blacklist_manager import BlacklistManager
20
33
  from .models import IPExemption
@@ -30,12 +43,22 @@ MODEL_PATH = getattr(
30
43
  def load_model_safely():
31
44
  """Load the AI model with version compatibility checking."""
32
45
  import warnings
33
- import sklearn
34
46
 
35
47
  # Check if AI is disabled globally
36
48
  ai_disabled = getattr(settings, "AIWAF_DISABLE_AI", False)
37
49
  if ai_disabled:
38
- print("AI functionality disabled via AIWAF_DISABLE_AI setting")
50
+ print("ℹ️ AI functionality disabled via AIWAF_DISABLE_AI setting")
51
+ return None
52
+
53
+ # Check if required dependencies are available
54
+ if not JOBLIB_AVAILABLE:
55
+ print("ℹ️ joblib not available, AI functionality disabled")
56
+ return None
57
+
58
+ try:
59
+ import sklearn
60
+ except ImportError:
61
+ print("ℹ️ sklearn not available, AI functionality disabled")
39
62
  return None
40
63
 
41
64
  try:
@@ -52,13 +75,13 @@ def load_model_safely():
52
75
  current_version = sklearn.__version__
53
76
 
54
77
  if stored_version != current_version:
55
- print(f"Model was trained with sklearn v{stored_version}, current v{current_version}")
78
+ print(f"ℹ️ Model was trained with sklearn v{stored_version}, current v{current_version}")
56
79
  print(" Run 'python manage.py detect_and_train' to update model if needed.")
57
80
 
58
81
  return model
59
82
  else:
60
83
  # Old format - direct model object
61
- print("Using legacy model format. Consider retraining for better compatibility.")
84
+ print("ℹ️ Using legacy model format. Consider retraining for better compatibility.")
62
85
  return model_data
63
86
 
64
87
  except Exception as e:
@@ -66,6 +89,7 @@ def load_model_safely():
66
89
  print("AI anomaly detection will be disabled until model is retrained.")
67
90
  print("Run 'python manage.py detect_and_train' to regenerate the model.")
68
91
  return None
92
+ return None
69
93
 
70
94
  # Load model with safety checks
71
95
  MODEL = load_model_safely()
@@ -529,20 +553,22 @@ class AIAnomalyMiddleware(MiddlewareMixin):
529
553
  burst_count = sum(1 for (t, _, _, _) in data if now - t <= 10)
530
554
  total_404 = sum(1 for (_, _, st, _) in data if st == 404)
531
555
  feats = [path_len, kw_hits, resp_time, status_idx, burst_count, total_404]
532
- X = np.array(feats, dtype=float).reshape(1, -1)
533
556
 
534
- # Only use AI model if it's available
535
- if self.model is not None and self.model.predict(X)[0] == -1:
536
- # AI detected anomaly - but analyze patterns before blocking (like trainer.py)
537
-
538
- # Get recent behavior data for this IP to make intelligent blocking decision
539
- recent_data = [d for d in data if now - d[0] <= 300] # Last 5 minutes
557
+ # Only use AI model if it's available and numpy is available
558
+ if self.model is not None and NUMPY_AVAILABLE:
559
+ X = np.array(feats, dtype=float).reshape(1, -1)
540
560
 
541
- if recent_data:
542
- # Calculate behavior metrics similar to trainer.py
543
- recent_kw_hits = []
544
- recent_404s = 0
545
- recent_burst_counts = []
561
+ if self.model.predict(X)[0] == -1:
562
+ # AI detected anomaly - but analyze patterns before blocking (like trainer.py)
563
+
564
+ # Get recent behavior data for this IP to make intelligent blocking decision
565
+ recent_data = [d for d in data if now - d[0] <= 300] # Last 5 minutes
566
+
567
+ if recent_data:
568
+ # Calculate behavior metrics similar to trainer.py
569
+ recent_kw_hits = []
570
+ recent_404s = 0
571
+ recent_burst_counts = []
546
572
 
547
573
  for entry_time, entry_path, entry_status, entry_resp_time in recent_data:
548
574
  # Calculate keyword hits for this entry
@@ -615,6 +641,110 @@ class AIAnomalyMiddleware(MiddlewareMixin):
615
641
 
616
642
  class HoneypotTimingMiddleware(MiddlewareMixin):
617
643
  MIN_FORM_TIME = getattr(settings, "AIWAF_MIN_FORM_TIME", 1.0) # seconds
644
+ MAX_PAGE_TIME = getattr(settings, "AIWAF_MAX_PAGE_TIME", 240) # 4 minutes default
645
+
646
+ def _view_accepts_method(self, request, method):
647
+ """Check if the current view/URL pattern accepts the specified HTTP method"""
648
+ try:
649
+ from django.urls import resolve
650
+ from django.urls.resolvers import URLResolver, URLPattern
651
+
652
+ # Resolve the current URL to get the view
653
+ resolved = resolve(request.path)
654
+ view_func = resolved.func
655
+
656
+ # Handle class-based views
657
+ if hasattr(view_func, 'cls'):
658
+ view_class = view_func.cls
659
+
660
+ # Check http_method_names attribute (most reliable)
661
+ if hasattr(view_class, 'http_method_names'):
662
+ allowed_methods = [m.upper() for m in view_class.http_method_names]
663
+ return method.upper() in allowed_methods
664
+
665
+ # Check for method-handling methods
666
+ method_handlers = {
667
+ 'GET': ['get'],
668
+ 'POST': ['post', 'form_valid', 'form_invalid'],
669
+ 'PUT': ['put'],
670
+ 'PATCH': ['patch'],
671
+ 'DELETE': ['delete']
672
+ }
673
+
674
+ if method.upper() in method_handlers:
675
+ handlers = method_handlers[method.upper()]
676
+ has_handler = any(hasattr(view_class, handler) for handler in handlers)
677
+ if has_handler:
678
+ return True
679
+
680
+ # If no handler found, check if it's a common method that should be rejected
681
+ if method.upper() in ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']:
682
+ return False
683
+
684
+ # Default: assume method is allowed for class-based views
685
+ return True
686
+
687
+ # Handle function-based views
688
+ else:
689
+ # Check if view has explicit allowed methods
690
+ if hasattr(view_func, 'http_method_names'):
691
+ allowed_methods = [m.upper() for m in view_func.http_method_names]
692
+ return method.upper() in allowed_methods
693
+
694
+ # For function-based views, inspect the source code
695
+ import inspect
696
+ try:
697
+ source = inspect.getsource(view_func)
698
+ method_upper = method.upper()
699
+
700
+ # Look for method handling in the source
701
+ if f'request.method' in source and method_upper in source:
702
+ return True
703
+
704
+ # Look for method-specific patterns
705
+ method_patterns = {
706
+ 'GET': ['request.GET', 'GET'],
707
+ 'POST': ['request.POST', 'POST', 'form.is_valid()'],
708
+ 'PUT': ['PUT', 'request.PUT'],
709
+ 'DELETE': ['DELETE', 'request.DELETE']
710
+ }
711
+
712
+ if method.upper() in method_patterns:
713
+ patterns = method_patterns[method.upper()]
714
+ if any(pattern in source for pattern in patterns):
715
+ return True
716
+
717
+ except (OSError, TypeError):
718
+ # Can't get source, make educated guess
719
+ pass
720
+
721
+ # Check URL pattern name for method-specific endpoints
722
+ if resolved.url_name:
723
+ url_name_lower = resolved.url_name.lower()
724
+
725
+ # POST-only patterns
726
+ post_only_patterns = ['create', 'submit', 'upload', 'process']
727
+ # GET-only patterns
728
+ get_only_patterns = ['list', 'detail', 'view', 'display']
729
+
730
+ if method.upper() == 'POST':
731
+ if any(pattern in url_name_lower for pattern in post_only_patterns):
732
+ return True
733
+ if any(pattern in url_name_lower for pattern in get_only_patterns):
734
+ return False
735
+ elif method.upper() == 'GET':
736
+ if any(pattern in url_name_lower for pattern in get_only_patterns):
737
+ return True
738
+ if any(pattern in url_name_lower for pattern in post_only_patterns):
739
+ return False
740
+
741
+ # Default: assume function-based views accept common methods
742
+ return method.upper() in ['GET', 'POST', 'HEAD', 'OPTIONS']
743
+
744
+ except Exception as e:
745
+ # If we can't determine, err on the side of caution and allow
746
+ print(f"AIWAF: Could not determine {method} capability for {request.path}: {e}")
747
+ return True
618
748
 
619
749
  def process_request(self, request):
620
750
  if is_exempt(request):
@@ -629,11 +759,33 @@ class HoneypotTimingMiddleware(MiddlewareMixin):
629
759
  return None
630
760
 
631
761
  if request.method == "GET":
762
+ # ENHANCEMENT: Check if this view accepts GET requests
763
+ if not self._view_accepts_method(request, 'GET'):
764
+ # This view is POST-only, but received a GET - likely scanning/probing
765
+ if not exemption_store.is_exempted(ip):
766
+ BlacklistManager.block(ip, f"GET to POST-only view: {request.path}")
767
+ if BlacklistManager.is_blocked(ip):
768
+ return JsonResponse({
769
+ "error": "blocked",
770
+ "message": f"GET not allowed for {request.path}"
771
+ }, status=405) # Method Not Allowed
772
+
632
773
  # Store timestamp for this IP's GET request
633
774
  # Use a general key for the IP, not path-specific
634
775
  cache.set(f"honeypot_get:{ip}", time.time(), timeout=300) # 5 min timeout
635
776
 
636
777
  elif request.method == "POST":
778
+ # ENHANCEMENT: Check if this view actually accepts POST requests
779
+ if not self._view_accepts_method(request, 'POST'):
780
+ # This view is GET-only, but received a POST - likely malicious
781
+ if not exemption_store.is_exempted(ip):
782
+ BlacklistManager.block(ip, f"POST to GET-only view: {request.path}")
783
+ if BlacklistManager.is_blocked(ip):
784
+ return JsonResponse({
785
+ "error": "blocked",
786
+ "message": f"POST not allowed for {request.path}"
787
+ }, status=405) # Method Not Allowed
788
+
637
789
  # Check if there was a preceding GET request
638
790
  get_time = cache.get(f"honeypot_get:{ip}")
639
791
 
@@ -654,6 +806,17 @@ class HoneypotTimingMiddleware(MiddlewareMixin):
654
806
  time_diff = time.time() - get_time
655
807
  min_time = self.MIN_FORM_TIME
656
808
 
809
+ # ENHANCEMENT 2: Check for page timeout (4+ minutes)
810
+ if time_diff > self.MAX_PAGE_TIME:
811
+ # Page has been open too long - suspicious or stale session
812
+ # Don't block immediately, but require a fresh page load
813
+ cache.delete(f"honeypot_get:{ip}") # Force fresh GET
814
+ return JsonResponse({
815
+ "error": "page_expired",
816
+ "message": "Page has expired. Please reload and try again.",
817
+ "reload_required": True
818
+ }, status=409) # 409 Conflict - client should reload
819
+
657
820
  # Use shorter time threshold for login paths (users can login quickly)
658
821
  if any(request.path.lower().startswith(login_path) for login_path in [
659
822
  "/admin/login/", "/login/", "/accounts/login/", "/auth/login/", "/signin/"
@@ -668,6 +831,19 @@ class HoneypotTimingMiddleware(MiddlewareMixin):
668
831
  if BlacklistManager.is_blocked(ip):
669
832
  return JsonResponse({"error": "blocked"}, status=403)
670
833
 
834
+ else:
835
+ # Handle other HTTP methods (PUT, DELETE, PATCH, etc.)
836
+ if request.method not in ['GET', 'POST', 'HEAD', 'OPTIONS']:
837
+ # Check if this view supports the requested method
838
+ if not self._view_accepts_method(request, request.method):
839
+ if not exemption_store.is_exempted(ip):
840
+ BlacklistManager.block(ip, f"{request.method} to view that doesn't support it: {request.path}")
841
+ if BlacklistManager.is_blocked(ip):
842
+ return JsonResponse({
843
+ "error": "blocked",
844
+ "message": f"{request.method} not allowed for {request.path}"
845
+ }, status=405) # Method Not Allowed
846
+
671
847
  return None
672
848
 
673
849
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiwaf
3
- Version: 0.1.9.3.0
3
+ Version: 0.1.9.3.1
4
4
  Summary: AI-powered Web Application Firewall
5
5
  Home-page: https://github.com/aayushgauba/aiwaf
6
6
  Author: Aayush Gauba
@@ -32,6 +32,8 @@ Dynamic: requires-python
32
32
  - ✅ **Granular Reset Commands** - Clear specific data types (`--blacklist`, `--keywords`, `--exemptions`)
33
33
  - ✅ **Context-Aware Learning** - Only learns from suspicious requests, not legitimate site functionality
34
34
  - ✅ **Enhanced Configuration** - `AIWAF_ALLOWED_PATH_KEYWORDS` and `AIWAF_EXEMPT_KEYWORDS`
35
+ - ✅ **Comprehensive HTTP Method Validation** - Blocks GET→POST-only, POST→GET-only, unsupported REST methods
36
+ - ✅ **Enhanced Honeypot Protection** - POST validation & 4-minute page timeout with smart reload detection
35
37
 
36
38
  ---
37
39
 
@@ -111,10 +113,16 @@ aiwaf/
111
113
  - **File‑Extension Probing Detection**
112
114
  Tracks repeated 404s on common extensions (e.g. `.php`, `.asp`) and blocks IPs.
113
115
 
114
- - **Timing-Based Honeypot**
115
- Tracks GET→POST timing patterns. Blocks IPs that:
116
+ - **Enhanced Timing-Based Honeypot**
117
+ Advanced GET→POST timing analysis with comprehensive HTTP method validation:
116
118
  - POST directly without a preceding GET request
117
119
  - Submit forms faster than `AIWAF_MIN_FORM_TIME` seconds (default: 1 second)
120
+ - **🆕 Smart HTTP Method Validation** - Comprehensive protection against method misuse:
121
+ - Blocks GET requests to POST-only views (form endpoints, API creates)
122
+ - Blocks POST requests to GET-only views (list pages, read-only content)
123
+ - Blocks unsupported REST methods (PUT/DELETE to non-REST views)
124
+ - Uses Django view analysis: class-based views, method handlers, URL patterns
125
+ - **🆕 Page expiration** after `AIWAF_MAX_PAGE_TIME` (4 minutes) with smart reload
118
126
 
119
127
  - **UUID Tampering Protection**
120
128
  Blocks guessed or invalid UUIDs that don't resolve to real models.
@@ -509,6 +517,7 @@ python manage.py aiwaf_logging --clear # Clear log files
509
517
  ```python
510
518
  AIWAF_MODEL_PATH = BASE_DIR / "aiwaf" / "resources" / "model.pkl"
511
519
  AIWAF_MIN_FORM_TIME = 1.0 # minimum seconds between GET and POST
520
+ AIWAF_MAX_PAGE_TIME = 240 # maximum page age before requiring reload (4 minutes)
512
521
  AIWAF_AI_CONTAMINATION = 0.05 # AI anomaly detection sensitivity (5%)
513
522
  AIWAF_RATE_WINDOW = 10 # seconds
514
523
  AIWAF_RATE_MAX = 20 # max requests per window
@@ -801,9 +810,36 @@ python manage.py aiwaf_reset --blacklist --confirm
801
810
  | IPAndKeywordBlockMiddleware | Blocks requests from known blacklisted IPs and Keywords |
802
811
  | RateLimitMiddleware | Enforces burst & flood thresholds |
803
812
  | AIAnomalyMiddleware | ML‑driven behavior analysis + block on anomaly |
804
- | HoneypotTimingMiddleware | Detects bots via GET→POST timing analysis |
813
+ | HoneypotTimingMiddleware | Enhanced bot detection: GET→POST timing, POST validation, page timeouts |
805
814
  | UUIDTamperMiddleware | Blocks guessed/nonexistent UUIDs across all models in an app |
806
815
 
816
+ ### 🍯 Enhanced Honeypot Protection
817
+
818
+ The **HoneypotTimingMiddleware** now includes advanced bot detection capabilities:
819
+
820
+ #### 🚫 Smart POST Request Validation
821
+ - **Analyzes Django views** to determine actual allowed HTTP methods
822
+ - **Intelligent detection** of GET-only vs POST-capable views
823
+ - **Example**: `POST` to view with `http_method_names = ['get']` → `403 Blocked`
824
+
825
+ #### ⏰ Page Timeout with Smart Reload
826
+ - **4-minute page expiration** prevents stale session attacks
827
+ - **HTTP 409 response** with reload instructions instead of immediate blocking
828
+ - **CSRF protection** by forcing fresh page loads for old sessions
829
+
830
+ ```python
831
+ # Configuration
832
+ AIWAF_MIN_FORM_TIME = 1.0 # Minimum form submission time
833
+ AIWAF_MAX_PAGE_TIME = 240 # Page timeout (4 minutes)
834
+ ```
835
+
836
+ **Timeline Example**:
837
+ ```
838
+ 12:00:00 - GET /contact/ ✅ Page loaded
839
+ 12:02:00 - POST /contact/ ✅ Valid submission (2 minutes)
840
+ 12:04:30 - POST /contact/ ❌ 409 Conflict (page expired, reload required)
841
+ ```
842
+
807
843
  ---
808
844
 
809
845
  ## Sponsors
@@ -1,8 +1,8 @@
1
- aiwaf/__init__.py,sha256=WP3mllfac0SjosCM1zXVVzv8UFzeD3o2rvtOl29Km4E,220
1
+ aiwaf/__init__.py,sha256=VlFbI8uqJmi1V0hsKasasV1BFglekVX0R5jvEOwXGzE,220
2
2
  aiwaf/apps.py,sha256=nCez-Ptlv2kaEk5HenA8b1pATz1VfhrHP1344gwcY1A,142
3
3
  aiwaf/blacklist_manager.py,sha256=LYCeKFB-7e_C6Bg2WeFJWFIIQlrfRMPuGp30ivrnhQY,1196
4
4
  aiwaf/decorators.py,sha256=IUKOdM_gdroffImRZep1g1wT6gNqD10zGwcp28hsJCs,825
5
- aiwaf/middleware.py,sha256=Bo8xcrRugp9audWNKp2nkrfYC34E4523tgWarEdlb2A,32256
5
+ aiwaf/middleware.py,sha256=BnVdA4g2YTDo5g_H1Q8EE-ctVR4JF_yV3PaLHMgYZ-E,40804
6
6
  aiwaf/middleware_logger.py,sha256=LWZVDAnjh6CGESirA8eMbhGgJKB7lVDGRQqVroH95Lo,4742
7
7
  aiwaf/models.py,sha256=vQxgY19BDVMjoO903UNrTZC1pNoLltMU6wbyWPoAEns,2719
8
8
  aiwaf/storage.py,sha256=pUXE3bm7aRrABh_B6jTOBUQOYK67oQmHaR9EqyOasis,14038
@@ -13,7 +13,7 @@ aiwaf/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJW
13
13
  aiwaf/management/commands/add_exemption.py,sha256=U_ByfJw1EstAZ8DaSoRb97IGwYzXs0DBJkVAqeN4Wak,1128
14
14
  aiwaf/management/commands/add_ipexemption.py,sha256=sSf3d9hGK9RqqlBYkCrnrd8KZWGT-derSpoWnEY4H60,952
15
15
  aiwaf/management/commands/aiwaf_diagnose.py,sha256=nXFRhq66N4QC3e4scYJ2sUngJce-0yDxtBO3R2BllRM,6134
16
- aiwaf/management/commands/aiwaf_list.py,sha256=tZK3FugApmPxxvmoB4-nLY9fpZJgiRtD137Bre5hEp8,3839
16
+ aiwaf/management/commands/aiwaf_list.py,sha256=DVuhrQ4OUKWlzcQERadhedSRBhedt6OGJrrUkKFIoG0,4821
17
17
  aiwaf/management/commands/aiwaf_logging.py,sha256=FCIqULn2tii2vD9VxL7vk3PV4k4vr7kaA00KyaCExYY,7692
18
18
  aiwaf/management/commands/aiwaf_reset.py,sha256=pcF0zOYDSqjpCwDtk2HYJZLgr76td8OFRENtl20c1dQ,7472
19
19
  aiwaf/management/commands/check_dependencies.py,sha256=GOZl00pDwW2cJjDvIaCeB3yWxmeYcJDRTIpmOTLvy2c,37204
@@ -29,8 +29,8 @@ aiwaf/management/commands/test_exemption_fix.py,sha256=ngyGaHUCmQQ6y--6j4q1viZJt
29
29
  aiwaf/resources/model.pkl,sha256=5t6h9BX8yoh2xct85MXOO60jdlWyg1APskUOW0jZE1Y,1288265
30
30
  aiwaf/templatetags/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
31
  aiwaf/templatetags/aiwaf_tags.py,sha256=XXfb7Tl4DjU3Sc40GbqdaqOEtKTUKELBEk58u83wBNw,357
32
- aiwaf-0.1.9.3.0.dist-info/licenses/LICENSE,sha256=Ir8PX4dxgAcdB0wqNPIkw84fzIIRKE75NoUil9RX0QU,1069
33
- aiwaf-0.1.9.3.0.dist-info/METADATA,sha256=VlEUQCrOdf1uJI5cUYc8piuTCQgbYG6WT6-tajYCDQA,27208
34
- aiwaf-0.1.9.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
35
- aiwaf-0.1.9.3.0.dist-info/top_level.txt,sha256=kU6EyjobT6UPCxuWpI_BvcHDG0I2tMgKaPlWzVxe2xI,6
36
- aiwaf-0.1.9.3.0.dist-info/RECORD,,
32
+ aiwaf-0.1.9.3.1.dist-info/licenses/LICENSE,sha256=Ir8PX4dxgAcdB0wqNPIkw84fzIIRKE75NoUil9RX0QU,1069
33
+ aiwaf-0.1.9.3.1.dist-info/METADATA,sha256=TQO1y9t5sRQY7zBNyJc6dvTfJ1JXC1vBTf1RjH8O8m0,29037
34
+ aiwaf-0.1.9.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
35
+ aiwaf-0.1.9.3.1.dist-info/top_level.txt,sha256=kU6EyjobT6UPCxuWpI_BvcHDG0I2tMgKaPlWzVxe2xI,6
36
+ aiwaf-0.1.9.3.1.dist-info/RECORD,,