GuardianUnivalle-Benito-Yucra 0.1.43__py3-none-any.whl → 0.1.45__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 GuardianUnivalle-Benito-Yucra might be problematic. Click here for more details.

@@ -1,16 +1,4 @@
1
- """
2
- CSRF Defense Middleware
3
- ========================
4
- Detecta y registra posibles ataques CSRF (Cross-Site Request Forgery).
5
-
6
- Algoritmos relacionados:
7
- * Uso de secreto aleatorio criptográfico (generar_token_csrf).
8
- * Validación simple por comparación (validar_token_csrf).
9
- * Contribución a fórmula de amenaza S:
10
- S_csrf = w_csrf * intentos_csrf
11
- S_csrf = 0.2 * 1
12
- """
13
-
1
+ # CSRF defense (parche recomendado)
14
2
  from __future__ import annotations
15
3
  import secrets
16
4
  import logging
@@ -21,9 +9,6 @@ from urllib.parse import urlparse
21
9
  from django.conf import settings
22
10
  from django.utils.deprecation import MiddlewareMixin
23
11
 
24
- # ======================================================
25
- # === CONFIGURACIÓN DE LOGGER ===
26
- # ======================================================
27
12
  logger = logging.getLogger("csrfdefense")
28
13
  logger.setLevel(logging.INFO)
29
14
  if not logger.handlers:
@@ -31,57 +16,33 @@ if not logger.handlers:
31
16
  handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
32
17
  logger.addHandler(handler)
33
18
 
34
-
35
- # ======================================================
36
- # === FUNCIONES AUXILIARES DE TOKEN CSRF ===
37
- # ======================================================
38
- def registrar_evento(tipo: str, mensaje: str):
39
- """Registra eventos importantes en los logs."""
40
- logger.warning(f"[{tipo}] {mensaje}")
41
-
42
-
43
- def generar_token_csrf() -> str:
44
- """Genera un token CSRF seguro."""
45
- token = secrets.token_hex(32)
46
- registrar_evento("CSRF", "Token CSRF generado")
47
- return token
48
-
49
-
50
- def validar_token_csrf(token: str, token_sesion: str) -> bool:
51
- """Valida que el token recibido coincida con el token en sesión."""
52
- valido = token == token_sesion
53
- if not valido:
54
- registrar_evento("CSRF", "Intento de CSRF detectado (token no coincide)")
55
- return valido
56
-
57
-
58
- # ======================================================
59
- # === CONSTANTES Y CONFIGURACIONES ===
60
- # ======================================================
61
19
  STATE_CHANGING_METHODS = {"POST", "PUT", "PATCH", "DELETE"}
62
- CSRF_HEADER_NAMES = (
63
- "HTTP_X_CSRFTOKEN",
64
- "HTTP_X_CSRF_TOKEN",
65
- )
20
+ CSRF_HEADER_NAMES = ("HTTP_X_CSRFTOKEN", "HTTP_X_CSRF_TOKEN")
66
21
  CSRF_COOKIE_NAME = getattr(settings, "CSRF_COOKIE_NAME", "csrftoken")
67
22
  POST_FIELD_NAME = "csrfmiddlewaretoken"
68
23
 
24
+ # Nota: NO consideramos 'application/json' sospechoso aquí por defecto,
25
+ # porque muchas APIs legítimas usan JSON.
69
26
  SUSPICIOUS_CT_PATTERNS = [
27
+ re.compile(r"text/plain", re.I),
70
28
  re.compile(r"application/x-www-form-urlencoded", re.I),
71
29
  re.compile(r"multipart/form-data", re.I),
72
30
  ]
73
31
 
32
+ # Umbral minimo de "señales" para marcar como ataque (configurable)
33
+ CSRF_DEFENSE_MIN_SIGNALS = getattr(settings, "CSRF_DEFENSE_MIN_SIGNALS", 1)
34
+ # Opción para excluir rutas de API que manejan JSON (cambia según tu proyecto)
35
+ CSRF_DEFENSE_EXCLUDED_API_PREFIXES = getattr(settings, "CSRF_DEFENSE_EXCLUDED_API_PREFIXES", ["/api/"])
74
36
 
75
- # ======================================================
76
- # === FUNCIONES DE APOYO ===
77
- # ======================================================
78
37
  def get_client_ip(request):
79
38
  x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
80
39
  if x_forwarded_for:
81
- return x_forwarded_for.split(",")[0].strip()
40
+ # toma la primera IP real
41
+ ips = [ip.strip() for ip in x_forwarded_for.split(",") if ip.strip()]
42
+ if ips:
43
+ return ips[0]
82
44
  return request.META.get("REMOTE_ADDR", "")
83
45
 
84
-
85
46
  def host_from_header(header_value: str) -> str | None:
86
47
  if not header_value:
87
48
  return None
@@ -93,52 +54,44 @@ def host_from_header(header_value: str) -> str | None:
93
54
  except Exception:
94
55
  return None
95
56
 
96
-
97
57
  def origin_matches_host(request) -> bool:
98
- """Verifica si Origin/Referer coinciden con Host."""
99
58
  host_header = request.META.get("HTTP_HOST") or request.META.get("SERVER_NAME")
100
59
  if not host_header:
101
60
  return True
102
-
103
61
  host = host_header.split(":")[0]
104
62
  origin = request.META.get("HTTP_ORIGIN", "")
105
63
  referer = request.META.get("HTTP_REFERER", "")
106
-
107
64
  origin_host = host_from_header(origin)
108
65
  referer_host = host_from_header(referer)
109
-
66
+ # bloquear obvious javascript: referers
67
+ if any(re.search(r"(javascript:|<script|data:text/html)", h or "", re.I) for h in [origin, referer]):
68
+ return False
110
69
  if origin_host and origin_host == host:
111
70
  return True
112
71
  if referer_host and referer_host == host:
113
72
  return True
73
+ # si no hay origin ni referer, lo consideramos neutral (no marcar)
114
74
  if not origin and not referer:
115
75
  return True
116
-
117
76
  return False
118
77
 
119
-
120
78
  def has_csrf_token(request) -> bool:
121
- """Comprueba si hay signos de token CSRF presente."""
79
+ # busca header, cookie o campo form
122
80
  for h in CSRF_HEADER_NAMES:
123
81
  if request.META.get(h):
124
82
  return True
125
-
126
83
  cookie_val = request.COOKIES.get(CSRF_COOKIE_NAME)
127
84
  if cookie_val:
128
85
  return True
129
-
130
86
  try:
131
87
  if request.method == "POST" and hasattr(request, "POST"):
132
88
  if request.POST.get(POST_FIELD_NAME):
133
89
  return True
134
90
  except Exception:
135
91
  pass
136
-
137
92
  return False
138
93
 
139
-
140
94
  def extract_payload_text(request) -> str:
141
- """Extrae contenido útil de la solicitud para análisis."""
142
95
  parts: List[str] = []
143
96
  try:
144
97
  body = request.body.decode("utf-8", errors="ignore")
@@ -153,18 +106,15 @@ def extract_payload_text(request) -> str:
153
106
  parts.append(request.META.get("HTTP_REFERER", ""))
154
107
  return " ".join([p for p in parts if p])
155
108
 
156
-
157
- # ======================================================
158
- # === MIDDLEWARE DE DEFENSA CSRF ===
159
- # ======================================================
160
109
  class CSRFDefenseMiddleware(MiddlewareMixin):
161
- """
162
- Middleware para DETECTAR intentos de CSRF:
163
- - Marca request.sql_attack_info con 'tipos': ['CSRF'] y 'descripcion' con razones.
164
- - No bloquea la petición directamente, permite que AuditoriaMiddleware lo maneje.
165
- """
166
-
167
110
  def process_request(self, request):
111
+ # 1) Excluir APIs JSON si se configuró así
112
+ for prefix in CSRF_DEFENSE_EXCLUDED_API_PREFIXES:
113
+ if request.path.startswith(prefix):
114
+ # debug log opcional
115
+ logger.debug(f"[CSRFDefense] Skip analysis for API prefix {prefix} path {request.path}")
116
+ return None
117
+
168
118
  client_ip = get_client_ip(request)
169
119
  trusted_ips = getattr(settings, "CSRF_DEFENSE_TRUSTED_IPS", [])
170
120
  if client_ip in trusted_ips:
@@ -187,24 +137,22 @@ class CSRFDefenseMiddleware(MiddlewareMixin):
187
137
 
188
138
  # 2) Origin/Referer no coinciden
189
139
  if not origin_matches_host(request):
190
- descripcion.append(
191
- "Origin/Referer no coinciden con Host (posible cross-site)"
192
- )
140
+ descripcion.append("Origin/Referer no coinciden con Host (posible cross-site)")
193
141
 
194
- # 3) Content-Type sospechoso
195
- content_type = request.META.get("CONTENT_TYPE", "") or ""
142
+ # 3) Content-Type sospechoso (solo marcaremos si coincide uno de los patterns)
143
+ content_type = (request.META.get("CONTENT_TYPE") or "")
196
144
  for patt in SUSPICIOUS_CT_PATTERNS:
197
145
  if patt.search(content_type):
198
146
  descripcion.append(f"Content-Type sospechoso: {content_type}")
199
147
  break
200
148
 
201
- # 4) Referer ausente
149
+ # 4) Referer ausente y sin header CSRF
202
150
  referer = request.META.get("HTTP_REFERER", "")
203
151
  if not referer and not any(request.META.get(h) for h in CSRF_HEADER_NAMES):
204
152
  descripcion.append("Referer ausente y sin X-CSRFToken")
205
153
 
206
- # Si hay señales, calculamos puntaje y registramos
207
- if descripcion:
154
+ # Si señales >= umbral entonces marcamos para auditoría
155
+ if descripcion and len(descripcion) >= CSRF_DEFENSE_MIN_SIGNALS:
208
156
  w_csrf = getattr(settings, "CSRF_DEFENSE_WEIGHT", 0.2)
209
157
  intentos_csrf = len(descripcion)
210
158
  s_csrf = w_csrf * intentos_csrf
@@ -218,15 +166,31 @@ class CSRFDefenseMiddleware(MiddlewareMixin):
218
166
  }
219
167
 
220
168
  logger.warning(
221
- "CSRF detectado desde IP %s: %s ; payload: %.200s ; score: %.2f",
222
- client_ip,
223
- descripcion,
224
- payload,
225
- s_csrf,
169
+ "CSRF detectado desde IP %s: %s ; path=%s ; Content-Type=%s ; score=%.2f",
170
+ client_ip, descripcion, request.path, content_type, s_csrf
226
171
  )
172
+ else:
173
+ # debug útil: saber por qué NO se marcó
174
+ if descripcion:
175
+ logger.debug(f"[CSRFDefense] low-signals ({len(descripcion)}) not marking: {descripcion}")
227
176
 
228
177
  return None
229
178
 
179
+ """
180
+ CSRF Defense Middleware
181
+ ========================
182
+ Detecta y registra posibles ataques CSRF (Cross-Site Request Forgery).
183
+
184
+ Algoritmos relacionados:
185
+ * Uso de secreto aleatorio criptográfico (generar_token_csrf).
186
+ * Validación simple por comparación (validar_token_csrf).
187
+ * Integración con detección XSS/SQL Injection mediante registro unificado.
188
+
189
+ Fórmula de amenaza:
190
+ S_csrf = w_csrf * intentos_csrf
191
+ S_csrf = 0.2 * 1
192
+ """
193
+
230
194
 
231
195
  """
232
196
  Algoritmos relacionados:
@@ -1,15 +1,16 @@
1
1
  # xss_defense.py
2
+ # GuardianUnivalle_Benito_Yucra/detectores/xss_defense.py
2
3
  from __future__ import annotations
3
4
  import json
4
5
  import logging
5
6
  import re
6
- from typing import List, Tuple
7
+ from typing import List, Tuple, Any, Dict
7
8
  from django.conf import settings
8
9
  from django.utils.deprecation import MiddlewareMixin
9
10
 
10
- # =====================================================
11
- # === CONFIGURACIÓN LOGGER ===
12
- # =====================================================
11
+ # -------------------------------------------------
12
+ # Logger
13
+ # -------------------------------------------------
13
14
  logger = logging.getLogger("xssdefense")
14
15
  logger.setLevel(logging.INFO)
15
16
  if not logger.handlers:
@@ -17,172 +18,263 @@ if not logger.handlers:
17
18
  handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
18
19
  logger.addHandler(handler)
19
20
 
20
- # =====================================================
21
- # === INTENTAR CARGAR BLEACH ===
22
- # =====================================================
21
+ # -------------------------------------------------
22
+ # Intentar usar bleach (si está instalado). Si no,
23
+ # seguimos con heurísticos de patrones.
24
+ # -------------------------------------------------
23
25
  try:
24
26
  import bleach
25
-
26
27
  _BLEACH_AVAILABLE = True
27
28
  except Exception:
28
29
  _BLEACH_AVAILABLE = False
29
30
 
30
- # =====================================================
31
- # === PATRONES DE DETECCIÓN XSS ===
32
- # =====================================================
33
- XSS_PATTERNS: List[Tuple[re.Pattern, str]] = [
34
- (re.compile(r"<\s*script\b", re.I), "Etiqueta <script>"),
35
- (re.compile(r"on\w+\s*=", re.I), "Atributo de evento (on*)"),
36
- (re.compile(r"javascript:\s*", re.I), "URI javascript:"),
37
- (re.compile(r"<\s*iframe\b", re.I), "Etiqueta <iframe>"),
38
- (re.compile(r"<\s*embed\b", re.I), "Etiqueta <embed>"),
39
- (re.compile(r"<\s*object\b", re.I), "Etiqueta <object>"),
40
- (re.compile(r"document\.cookie", re.I), "Acceso a document.cookie"),
41
- (re.compile(r"alert\s*\(", re.I), "Uso de alert() potencial"),
31
+ # -------------------------------------------------
32
+ # Patrones XSS con peso (descripcion, peso)
33
+ # - pesos mayores = más severo (por ejemplo <script> o javascript:)
34
+ # - esto permite un scoring acumulativo y menos falsos positivos
35
+ # -------------------------------------------------
36
+ XSS_PATTERNS: List[Tuple[re.Pattern, str, float]] = [
37
+ (re.compile(r"<\s*script\b", re.I), "Etiqueta <script>", 0.8),
38
+ (re.compile(r"javascript\s*:", re.I), "URI javascript:", 0.7),
39
+ (re.compile(r"<\s*iframe\b", re.I), "Etiqueta <iframe>", 0.7),
40
+ (re.compile(r"<\s*embed\b", re.I), "Etiqueta <embed>", 0.7),
41
+ (re.compile(r"<\s*object\b", re.I), "Etiqueta <object>", 0.7),
42
+ (re.compile(r"on\w+\s*=", re.I), "Atributo de evento (on*)", 0.5),
43
+ (re.compile(r"document\.cookie", re.I), "Acceso a document.cookie", 0.6),
44
+ (re.compile(r"alert\s*\(", re.I), "Uso de alert() potencial", 0.4),
45
+ # patrón para imágenes con onerror u onload (caso común)
46
+ (re.compile(r"<\s*img\b[^>]*on\w+\s*=", re.I), "Imagen con evento on*", 0.6),
42
47
  ]
43
48
 
49
+ # -------------------------------------------------
50
+ # Campos que NO queremos analizar (contraseñas, tokens, etc.)
51
+ # -------------------------------------------------
52
+ IGNORED_FIELDS = getattr(settings, "XSS_DEFENSE_IGNORED_FIELDS", ["password", "csrfmiddlewaretoken", "token", "auth"])
44
53
 
45
- # =====================================================
46
- # === FUNCIONES AUXILIARES XSS ===
47
- # =====================================================
48
- def detect_xss_text(text: str) -> Tuple[bool, List[str]]:
49
- """
50
- Busca patrones de XSS conocidos dentro de un texto.
51
- Devuelve (True, lista_de_coincidencias) si hay indicios.
52
- """
53
- matches: List[str] = []
54
- if not text:
55
- return False, matches
54
+ # Umbral por defecto para considerar "alto riesgo" (Auditoria puede bloquear según su lógica)
55
+ XSS_DEFENSE_THRESHOLD = getattr(settings, "XSS_DEFENSE_THRESHOLD", 0.6)
56
56
 
57
- for patt, message in XSS_PATTERNS:
58
- if patt.search(text):
59
- matches.append(message)
60
57
 
61
- return len(matches) > 0, matches
58
+ # -------------------------------------------------
59
+ # Util: validación / extracción de IP (robusta)
60
+ # -------------------------------------------------
61
+ def _is_valid_ip(ip: str) -> bool:
62
+ """Verifica que la cadena sea una IP válida (v4 o v6)."""
63
+ try:
64
+ import ipaddress
65
+ ipaddress.ip_address(ip)
66
+ return True
67
+ except Exception:
68
+ return False
62
69
 
63
70
 
64
- def sanitize_input_basic(text: str) -> str:
71
+ def get_client_ip(request) -> str:
65
72
  """
66
- Sanitiza una cadena eliminando etiquetas o caracteres peligrosos.
67
- Usa bleach si está disponible, de lo contrario hace escape manual.
73
+ Obtiene la mejor estimación de la IP del cliente:
74
+ - Revisa X-Forwarded-For (primera IP no vacía).
75
+ - Luego X-Real-IP, CF-Connecting-IP.
76
+ - Finalmente REMOTE_ADDR como fallback.
68
77
  """
69
- if text is None:
70
- return text
71
-
72
- if _BLEACH_AVAILABLE:
73
- return bleach.clean(text, tags=[], attributes={}, protocols=[], strip=True)
74
-
75
- replacements = [
76
- ("&", "&amp;"),
77
- ("<", "&lt;"),
78
- (">", "&gt;"),
79
- ('"', "&quot;"),
80
- ("'", "&#x27;"),
81
- ("/", "&#x2F;"),
82
- ]
83
- result = text
84
- for old, new in replacements:
85
- result = result.replace(old, new)
86
- return result
87
-
88
-
89
- def extract_payload_text(request) -> str:
78
+ # Preferir X-Forwarded-For
79
+ xff = request.META.get("HTTP_X_FORWARDED_FOR")
80
+ if xff:
81
+ # "client, proxy1, proxy2" => tomar la primera no vacía
82
+ parts = [p.strip() for p in xff.split(",") if p.strip()]
83
+ if parts:
84
+ return parts[0]
85
+
86
+ # Otros encabezados comunes
87
+ for h in ("HTTP_X_REAL_IP", "HTTP_CF_CONNECTING_IP", "HTTP_CLIENT_IP"):
88
+ v = request.META.get(h)
89
+ if v and _is_valid_ip(v):
90
+ return v
91
+
92
+ # Fallback
93
+ remote = request.META.get("REMOTE_ADDR")
94
+ return remote or ""
95
+
96
+
97
+ # -------------------------------------------------
98
+ # Extraer payload pero evitando cabeceras (para reducir falsos positivos)
99
+ # - Devuelve dict si es JSON, o dict con 'raw' para otros cuerpos
100
+ # - NO añade User-Agent o Referer al texto a analizar
101
+ # -------------------------------------------------
102
+ def extract_body_as_map(request) -> Dict[str, Any]:
90
103
  """
91
- Extrae un texto combinado con información del cuerpo, querystring,
92
- agente de usuario y referer para análisis XSS.
104
+ Extrae un diccionario con los datos a analizar:
105
+ - Si JSON: devuelve el dict JSON.
106
+ - Si form-data: devuelve request.POST.dict()
107
+ - Si otro: devuelve {'raw': <texto>}
93
108
  """
94
- parts: List[str] = []
95
-
96
109
  try:
97
- content_type = request.META.get("CONTENT_TYPE", "")
98
-
99
- if "application/json" in content_type:
100
- body_data = json.loads(request.body.decode("utf-8") or "{}")
101
- parts.append(json.dumps(body_data, ensure_ascii=False))
110
+ ct = request.META.get("CONTENT_TYPE", "")
111
+ if "application/json" in ct:
112
+ raw = request.body.decode("utf-8") or "{}"
113
+ try:
114
+ data = json.loads(raw)
115
+ if isinstance(data, dict):
116
+ return data
117
+ else:
118
+ # si el JSON no es un objeto (ej: lista), lo devolvemos como raw
119
+ return {"raw": raw}
120
+ except Exception:
121
+ return {"raw": raw}
102
122
  else:
103
- body_text = request.body.decode("utf-8", errors="ignore")
104
- if body_text:
105
- parts.append(body_text)
123
+ # FORM data (request.POST) u otros
124
+ try:
125
+ post = request.POST.dict()
126
+ if post:
127
+ return post
128
+ except Exception:
129
+ pass
130
+ # fallback: cuerpo crudo
131
+ raw = request.body.decode("utf-8", errors="ignore")
132
+ if raw:
133
+ return {"raw": raw}
106
134
  except Exception:
107
135
  pass
136
+ return {}
108
137
 
109
- qs = request.META.get("QUERY_STRING", "")
110
- if qs:
111
- parts.append(qs)
112
138
 
113
- parts.append(request.META.get("HTTP_USER_AGENT", ""))
114
- parts.append(request.META.get("HTTP_REFERER", ""))
139
+ # -------------------------------------------------
140
+ # Analizar un solo valor (string) en busca de XSS usando patrones
141
+ # Devuelve (score, descripciones, matches_patterns)
142
+ # -------------------------------------------------
143
+ def detect_xss_in_value(value: str) -> Tuple[float, List[str], List[str]]:
144
+ """
145
+ Analiza una cadena y devuelve:
146
+ - score acumulado (sum pesos)
147
+ - lista de descripciones activadas
148
+ - lista de patrones (regex.pattern) que matchearon
149
+ """
150
+ if not value:
151
+ return 0.0, [], []
115
152
 
116
- return " ".join([p for p in parts if p])
153
+ score_total = 0.0
154
+ descripcion = []
155
+ matches = []
117
156
 
157
+ # Si bleach está disponible, podemos "limpiar" y comparar; pero aquí solo detectamos
158
+ for patt, msg, weight in XSS_PATTERNS:
159
+ if patt.search(value):
160
+ score_total += weight
161
+ descripcion.append(msg)
162
+ matches.append(patt.pattern)
118
163
 
119
- # =====================================================
120
- # === MIDDLEWARE DE DEFENSA XSS ===
121
- # =====================================================
164
+ return round(score_total, 3), descripcion, matches
165
+
166
+
167
+ # -------------------------------------------------
168
+ # Middleware principal XSS
169
+ # -------------------------------------------------
122
170
  class XSSDefenseMiddleware(MiddlewareMixin):
123
171
  """
124
- Middleware profesional de detección XSS.
125
- - Detecta patrones maliciosos en solicitudes sospechosas.
126
- - No bloquea directamente, solo marca el ataque para auditoría.
127
- - Se integra con AuditoriaMiddleware (request.sql_attack_info).
172
+ Middleware para detección XSS.
173
+ - Analiza el body (campo por campo) y querystring si aplica.
174
+ - Ignora campos sensibles (password, token).
175
+ - No incluye User-Agent/Referer en el texto analizado (evita falsos positivos).
176
+ - Añade request.xss_attack_info con: ip, tipos, descripcion, payload, score, url.
128
177
  """
129
178
 
130
179
  def process_request(self, request):
131
- # ---------------------------------------------
132
- # 1. Filtrar IPs de confianza
133
- # ---------------------------------------------
180
+ # 1) IP y exclusiones
181
+ client_ip = get_client_ip(request)
134
182
  trusted_ips: List[str] = getattr(settings, "XSS_DEFENSE_TRUSTED_IPS", [])
135
- client_ip = request.META.get("REMOTE_ADDR", "")
136
- if client_ip in trusted_ips:
183
+ if client_ip and client_ip in trusted_ips:
137
184
  return None
138
185
 
139
- # ---------------------------------------------
140
- # 2. Excluir rutas seguras
141
- # ---------------------------------------------
142
186
  excluded_paths: List[str] = getattr(settings, "XSS_DEFENSE_EXCLUDED_PATHS", [])
143
187
  if any(request.path.startswith(p) for p in excluded_paths):
144
188
  return None
145
189
 
146
- # ---------------------------------------------
147
- # 3. Extraer y analizar payload
148
- # ---------------------------------------------
149
- payload = extract_payload_text(request)
150
- if not payload:
190
+ # 2) Extraer datos para analizar (dict)
191
+ data = extract_body_as_map(request)
192
+
193
+ # Incluir querystring (como campo separado) para análisis si existe
194
+ qs = request.META.get("QUERY_STRING", "")
195
+ if qs:
196
+ data["_query_string"] = qs
197
+
198
+ if not data:
151
199
  return None
152
200
 
153
- flagged, matches = detect_xss_text(payload)
154
- if not flagged:
201
+ total_score = 0.0
202
+ all_descriptions: List[str] = []
203
+ all_matches: List[str] = []
204
+ # payload_for_storage: guardamos un resumen/truncado para auditoría
205
+ payload_summary = []
206
+
207
+ # 3) Analizar campo por campo (si es dict) o el raw
208
+ if isinstance(data, dict):
209
+ for key, value in data.items():
210
+ # evitar analizar campos sensibles
211
+ if isinstance(key, str) and key.lower() in [f.lower() for f in IGNORED_FIELDS]:
212
+ continue
213
+
214
+ # convertir a string si es otro tipo (list, int...)
215
+ if isinstance(value, (dict, list)):
216
+ try:
217
+ vtext = json.dumps(value, ensure_ascii=False)
218
+ except Exception:
219
+ vtext = str(value)
220
+ else:
221
+ vtext = str(value or "")
222
+
223
+ # salto rápido: si el valor parece ser un email o password muy corto y sin signos,
224
+ # las probabilidades de XSS son muy bajas; continúa (reduce falsos positivos).
225
+ if key.lower() in ("email", "username") and len(vtext) < 256:
226
+ # aún así pasar por patrones (no lo ignoramos completamente), pero podemos bajar sensibilidad
227
+ pass
228
+
229
+ s, descs, matches = detect_xss_in_value(vtext)
230
+ total_score += s
231
+ all_descriptions.extend(descs)
232
+ all_matches.extend(matches)
233
+
234
+ if s > 0:
235
+ # almacenar fragmento del campo para auditoría (truncado)
236
+ payload_summary.append({ "field": key, "snippet": vtext[:300] })
237
+
238
+ else:
239
+ # si no es dict, analizar el raw como texto
240
+ raw = str(data)
241
+ s, descs, matches = detect_xss_in_value(raw)
242
+ total_score += s
243
+ all_descriptions.extend(descs)
244
+ all_matches.extend(matches)
245
+ if s > 0:
246
+ payload_summary.append({"field":"raw","snippet": raw[:500]})
247
+
248
+ # 4) si no detectó nada, continuar
249
+ if total_score == 0:
155
250
  return None
156
251
 
157
- # ---------------------------------------------
158
- # 4. Calcular puntaje de amenaza S_xss
159
- # ---------------------------------------------
160
- w_xss = getattr(settings, "XSS_DEFENSE_WEIGHT", 0.3)
161
- detecciones_xss = len(matches)
162
- s_xss = w_xss * detecciones_xss
252
+ # 5) construir info para auditoría (truncada)
253
+ url = request.build_absolute_uri()
254
+ score_rounded = round(total_score, 3)
255
+ payload_for_request = json.dumps(payload_summary, ensure_ascii=False)[:2000]
163
256
 
164
- # ---------------------------------------------
165
- # 5. Loggear y marcar en el request
166
- # ---------------------------------------------
167
257
  logger.warning(
168
- "XSS detectado desde IP %s: %s ; payload: %.200s ; score: %.2f",
258
+ "XSS detectado desde IP %s URL=%s Score=%.3f Desc=%s",
169
259
  client_ip,
170
- matches,
171
- payload,
172
- s_xss,
260
+ url,
261
+ score_rounded,
262
+ all_descriptions,
173
263
  )
174
264
 
265
+ # 6) marcar en el request (AuditoriaMiddleware lo consumirá)
175
266
  request.xss_attack_info = {
176
267
  "ip": client_ip,
177
268
  "tipos": ["XSS"],
178
- "descripcion": matches,
179
- "payload": payload,
180
- "score": s_xss,
269
+ "descripcion": all_descriptions,
270
+ "payload": payload_for_request,
271
+ "score": score_rounded,
272
+ "url": url,
181
273
  }
182
274
 
275
+ # 7) NO bloquear aquí — lo hace AuditoriaMiddleware según su política
183
276
  return None
184
277
 
185
-
186
278
  # =====================================================
187
279
  # === INFORMACIÓN EXTRA ===
188
280
  # =====================================================
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: GuardianUnivalle-Benito-Yucra
3
- Version: 0.1.43
3
+ Version: 0.1.45
4
4
  Summary: Middleware y detectores de seguridad (SQLi, XSS, CSRF, DoS, Keylogger) para Django/Flask
5
5
  Author-email: Andres Benito Calle Yucra <benitoandrescalle035@gmail.com>
6
6
  License: MIT
@@ -4,17 +4,17 @@ GuardianUnivalle_Benito_Yucra/auditoria/registro_auditoria.py,sha256=NnKBOeRWkXV
4
4
  GuardianUnivalle_Benito_Yucra/criptografia/cifrado_aead.py,sha256=wfoRpaKvOqPbollNQsDNUNWClYJlXYTKTYvv0qcR6aI,962
5
5
  GuardianUnivalle_Benito_Yucra/criptografia/intercambio_claves.py,sha256=9djnlzb022hUhrDbQyWz7lWLbkn_vQZ4K7qar1FXYmo,829
6
6
  GuardianUnivalle_Benito_Yucra/criptografia/kdf.py,sha256=_sbepEY1qHEKga0ExrX2WRg1HeCPY5MC5CfXZWYyl-A,709
7
- GuardianUnivalle_Benito_Yucra/detectores/detector_csrf.py,sha256=wt9LRMG9XK4eSSmW91tlGmsJWfyk445b8-n2oxlXlwo,7893
7
+ GuardianUnivalle_Benito_Yucra/detectores/detector_csrf.py,sha256=q7-UsVseTtIYZz4bbpx2X0kzpDmu2Cetm7eYPJtsruA,7608
8
8
  GuardianUnivalle_Benito_Yucra/detectores/detector_dos.py,sha256=l_JYCmRYpsXt1ZauNPF_wy5uGJhmunRbtJ_WKpC3Otc,6953
9
9
  GuardianUnivalle_Benito_Yucra/detectores/detector_keylogger.py,sha256=L5RQ0Sdgg7hTU1qkZYwt7AcDqtAzT6u-jwBGo7YWfsw,8078
10
10
  GuardianUnivalle_Benito_Yucra/detectores/detector_sql.py,sha256=EEbnn5J7sZxnsA2a0cT1VAB4ZS7BMhQiHSeqrR2SU3A,4820
11
- GuardianUnivalle_Benito_Yucra/detectores/detector_xss.py,sha256=Ipw1XXBd1-SsOOhhl9joQGduTq0GhSa61TRTyASF3XE,6795
11
+ GuardianUnivalle_Benito_Yucra/detectores/detector_xss.py,sha256=EDxGDaOosFJCyWTS_HkB300qL30ArxAEi-i0cVrzXyU,11027
12
12
  GuardianUnivalle_Benito_Yucra/middleware_web/middleware_web.py,sha256=23pLLYqliUoMrIC6ZEwz3hKXeDjWfHSm9vYPWGmDDik,495
13
13
  GuardianUnivalle_Benito_Yucra/mitigacion/limitador_peticion.py,sha256=ipMOebYhql-6mSyHs0ddYXOcXq9w8P_IXLlpiIqGncw,246
14
14
  GuardianUnivalle_Benito_Yucra/mitigacion/lista_bloqueo.py,sha256=6AYWII4mrmwCLHCvGTyoBxR4Oasr4raSHpFbVjqn7d8,193
15
15
  GuardianUnivalle_Benito_Yucra/puntuacion/puntuacion_amenaza.py,sha256=Wx5XfcII4oweLvZsTBEJ7kUc9pMpP5-36RfI5C5KJXo,561
16
- guardianunivalle_benito_yucra-0.1.43.dist-info/licenses/LICENSE,sha256=5e4IdL542v1E8Ft0A24GZjrxZeTsVK7XrS3mZEUhPtM,37
17
- guardianunivalle_benito_yucra-0.1.43.dist-info/METADATA,sha256=liLKkhHpRW9wdYFAD8zmbj86Gn6YIE1mSXjpPkhws_E,1893
18
- guardianunivalle_benito_yucra-0.1.43.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
- guardianunivalle_benito_yucra-0.1.43.dist-info/top_level.txt,sha256=HTWfZM64WAV_QYr5cnXnLuabQt92dvlxqlR3pCwpbDQ,30
20
- guardianunivalle_benito_yucra-0.1.43.dist-info/RECORD,,
16
+ guardianunivalle_benito_yucra-0.1.45.dist-info/licenses/LICENSE,sha256=5e4IdL542v1E8Ft0A24GZjrxZeTsVK7XrS3mZEUhPtM,37
17
+ guardianunivalle_benito_yucra-0.1.45.dist-info/METADATA,sha256=XUUVE_QmQaisJ7mmP4kV42K68pCcTtNjyEiip4DzCdw,1893
18
+ guardianunivalle_benito_yucra-0.1.45.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
19
+ guardianunivalle_benito_yucra-0.1.45.dist-info/top_level.txt,sha256=HTWfZM64WAV_QYr5cnXnLuabQt92dvlxqlR3pCwpbDQ,30
20
+ guardianunivalle_benito_yucra-0.1.45.dist-info/RECORD,,