GuardianUnivalle-Benito-Yucra 0.1.43__tar.gz → 0.1.44__tar.gz

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.

Files changed (26) hide show
  1. {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.44}/GuardianUnivalle_Benito_Yucra/detectores/detector_csrf.py +34 -41
  2. guardianunivalle_benito_yucra-0.1.44/GuardianUnivalle_Benito_Yucra/detectores/detector_xss.py +289 -0
  3. {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.44}/GuardianUnivalle_Benito_Yucra.egg-info/PKG-INFO +1 -1
  4. {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.44}/PKG-INFO +1 -1
  5. {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.44}/pyproject.toml +1 -1
  6. guardianunivalle_benito_yucra-0.1.43/GuardianUnivalle_Benito_Yucra/detectores/detector_xss.py +0 -197
  7. {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.44}/GuardianUnivalle_Benito_Yucra/__init__.py +0 -0
  8. {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.44}/GuardianUnivalle_Benito_Yucra/auditoria/registro_auditoria.py +0 -0
  9. {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.44}/GuardianUnivalle_Benito_Yucra/criptografia/cifrado_aead.py +0 -0
  10. {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.44}/GuardianUnivalle_Benito_Yucra/criptografia/intercambio_claves.py +0 -0
  11. {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.44}/GuardianUnivalle_Benito_Yucra/criptografia/kdf.py +0 -0
  12. {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.44}/GuardianUnivalle_Benito_Yucra/detectores/detector_dos.py +0 -0
  13. {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.44}/GuardianUnivalle_Benito_Yucra/detectores/detector_keylogger.py +0 -0
  14. {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.44}/GuardianUnivalle_Benito_Yucra/detectores/detector_sql.py +0 -0
  15. {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.44}/GuardianUnivalle_Benito_Yucra/middleware_web/middleware_web.py +0 -0
  16. {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.44}/GuardianUnivalle_Benito_Yucra/mitigacion/limitador_peticion.py +0 -0
  17. {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.44}/GuardianUnivalle_Benito_Yucra/mitigacion/lista_bloqueo.py +0 -0
  18. {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.44}/GuardianUnivalle_Benito_Yucra/puntuacion/puntuacion_amenaza.py +0 -0
  19. {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.44}/GuardianUnivalle_Benito_Yucra/utilidades.py +0 -0
  20. {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.44}/GuardianUnivalle_Benito_Yucra.egg-info/SOURCES.txt +0 -0
  21. {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.44}/GuardianUnivalle_Benito_Yucra.egg-info/dependency_links.txt +0 -0
  22. {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.44}/GuardianUnivalle_Benito_Yucra.egg-info/requires.txt +0 -0
  23. {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.44}/GuardianUnivalle_Benito_Yucra.egg-info/top_level.txt +0 -0
  24. {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.44}/LICENSE +0 -0
  25. {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.44}/README.md +0 -0
  26. {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.44}/setup.cfg +0 -0
@@ -1,15 +1,3 @@
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
 
14
2
  from __future__ import annotations
15
3
  import secrets
@@ -31,7 +19,6 @@ if not logger.handlers:
31
19
  handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
32
20
  logger.addHandler(handler)
33
21
 
34
-
35
22
  # ======================================================
36
23
  # === FUNCIONES AUXILIARES DE TOKEN CSRF ===
37
24
  # ======================================================
@@ -39,14 +26,12 @@ def registrar_evento(tipo: str, mensaje: str):
39
26
  """Registra eventos importantes en los logs."""
40
27
  logger.warning(f"[{tipo}] {mensaje}")
41
28
 
42
-
43
29
  def generar_token_csrf() -> str:
44
30
  """Genera un token CSRF seguro."""
45
31
  token = secrets.token_hex(32)
46
32
  registrar_evento("CSRF", "Token CSRF generado")
47
33
  return token
48
34
 
49
-
50
35
  def validar_token_csrf(token: str, token_sesion: str) -> bool:
51
36
  """Valida que el token recibido coincida con el token en sesión."""
52
37
  valido = token == token_sesion
@@ -54,35 +39,32 @@ def validar_token_csrf(token: str, token_sesion: str) -> bool:
54
39
  registrar_evento("CSRF", "Intento de CSRF detectado (token no coincide)")
55
40
  return valido
56
41
 
57
-
58
42
  # ======================================================
59
43
  # === CONSTANTES Y CONFIGURACIONES ===
60
44
  # ======================================================
61
45
  STATE_CHANGING_METHODS = {"POST", "PUT", "PATCH", "DELETE"}
62
- CSRF_HEADER_NAMES = (
63
- "HTTP_X_CSRFTOKEN",
64
- "HTTP_X_CSRF_TOKEN",
65
- )
46
+ CSRF_HEADER_NAMES = ("HTTP_X_CSRFTOKEN", "HTTP_X_CSRF_TOKEN")
66
47
  CSRF_COOKIE_NAME = getattr(settings, "CSRF_COOKIE_NAME", "csrftoken")
67
48
  POST_FIELD_NAME = "csrfmiddlewaretoken"
68
49
 
50
+ # Content-Type realmente sospechosos
69
51
  SUSPICIOUS_CT_PATTERNS = [
70
- re.compile(r"application/x-www-form-urlencoded", re.I),
71
- re.compile(r"multipart/form-data", re.I),
52
+ re.compile(r"text/plain", re.I),
53
+ re.compile(r"application/json", re.I),
72
54
  ]
73
55
 
74
-
75
56
  # ======================================================
76
57
  # === FUNCIONES DE APOYO ===
77
58
  # ======================================================
78
59
  def get_client_ip(request):
60
+ """Obtiene la IP real del cliente."""
79
61
  x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
80
62
  if x_forwarded_for:
81
63
  return x_forwarded_for.split(",")[0].strip()
82
64
  return request.META.get("REMOTE_ADDR", "")
83
65
 
84
-
85
66
  def host_from_header(header_value: str) -> str | None:
67
+ """Extrae el host limpio desde una cabecera."""
86
68
  if not header_value:
87
69
  return None
88
70
  try:
@@ -93,7 +75,6 @@ def host_from_header(header_value: str) -> str | None:
93
75
  except Exception:
94
76
  return None
95
77
 
96
-
97
78
  def origin_matches_host(request) -> bool:
98
79
  """Verifica si Origin/Referer coinciden con Host."""
99
80
  host_header = request.META.get("HTTP_HOST") or request.META.get("SERVER_NAME")
@@ -107,6 +88,11 @@ def origin_matches_host(request) -> bool:
107
88
  origin_host = host_from_header(origin)
108
89
  referer_host = host_from_header(referer)
109
90
 
91
+ # Detectar valores con scripts o URLs maliciosas
92
+ if any(re.search(r"(javascript:|<script|data:text/html)", h or "", re.I)
93
+ for h in [origin, referer]):
94
+ return False
95
+
110
96
  if origin_host and origin_host == host:
111
97
  return True
112
98
  if referer_host and referer_host == host:
@@ -116,9 +102,8 @@ def origin_matches_host(request) -> bool:
116
102
 
117
103
  return False
118
104
 
119
-
120
105
  def has_csrf_token(request) -> bool:
121
- """Comprueba si hay signos de token CSRF presente."""
106
+ """Comprueba si hay un token CSRF presente."""
122
107
  for h in CSRF_HEADER_NAMES:
123
108
  if request.META.get(h):
124
109
  return True
@@ -136,9 +121,8 @@ def has_csrf_token(request) -> bool:
136
121
 
137
122
  return False
138
123
 
139
-
140
124
  def extract_payload_text(request) -> str:
141
- """Extrae contenido útil de la solicitud para análisis."""
125
+ """Extrae el cuerpo útil de la solicitud."""
142
126
  parts: List[str] = []
143
127
  try:
144
128
  body = request.body.decode("utf-8", errors="ignore")
@@ -153,15 +137,14 @@ def extract_payload_text(request) -> str:
153
137
  parts.append(request.META.get("HTTP_REFERER", ""))
154
138
  return " ".join([p for p in parts if p])
155
139
 
156
-
157
140
  # ======================================================
158
141
  # === MIDDLEWARE DE DEFENSA CSRF ===
159
142
  # ======================================================
160
143
  class CSRFDefenseMiddleware(MiddlewareMixin):
161
144
  """
162
145
  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.
146
+ - Registra request.csrf_attack_info con 'tipos': ['CSRF'] y 'descripcion' con las razones.
147
+ - No bloquea directamente: deja que AuditoriaMiddleware lo maneje.
165
148
  """
166
149
 
167
150
  def process_request(self, request):
@@ -187,9 +170,7 @@ class CSRFDefenseMiddleware(MiddlewareMixin):
187
170
 
188
171
  # 2) Origin/Referer no coinciden
189
172
  if not origin_matches_host(request):
190
- descripcion.append(
191
- "Origin/Referer no coinciden con Host (posible cross-site)"
192
- )
173
+ descripcion.append("Origin/Referer no coinciden con Host (posible cross-site)")
193
174
 
194
175
  # 3) Content-Type sospechoso
195
176
  content_type = request.META.get("CONTENT_TYPE", "") or ""
@@ -198,12 +179,12 @@ class CSRFDefenseMiddleware(MiddlewareMixin):
198
179
  descripcion.append(f"Content-Type sospechoso: {content_type}")
199
180
  break
200
181
 
201
- # 4) Referer ausente
182
+ # 4) Referer ausente y sin token
202
183
  referer = request.META.get("HTTP_REFERER", "")
203
184
  if not referer and not any(request.META.get(h) for h in CSRF_HEADER_NAMES):
204
185
  descripcion.append("Referer ausente y sin X-CSRFToken")
205
186
 
206
- # Si hay señales, calculamos puntaje y registramos
187
+ # === Registro del ataque detectado ===
207
188
  if descripcion:
208
189
  w_csrf = getattr(settings, "CSRF_DEFENSE_WEIGHT", 0.2)
209
190
  intentos_csrf = len(descripcion)
@@ -219,14 +200,26 @@ class CSRFDefenseMiddleware(MiddlewareMixin):
219
200
 
220
201
  logger.warning(
221
202
  "CSRF detectado desde IP %s: %s ; payload: %.200s ; score: %.2f",
222
- client_ip,
223
- descripcion,
224
- payload,
225
- s_csrf,
203
+ client_ip, descripcion, payload, s_csrf,
226
204
  )
227
205
 
228
206
  return None
229
207
 
208
+ """
209
+ CSRF Defense Middleware
210
+ ========================
211
+ Detecta y registra posibles ataques CSRF (Cross-Site Request Forgery).
212
+
213
+ Algoritmos relacionados:
214
+ * Uso de secreto aleatorio criptográfico (generar_token_csrf).
215
+ * Validación simple por comparación (validar_token_csrf).
216
+ * Integración con detección XSS/SQL Injection mediante registro unificado.
217
+
218
+ Fórmula de amenaza:
219
+ S_csrf = w_csrf * intentos_csrf
220
+ S_csrf = 0.2 * 1
221
+ """
222
+
230
223
 
231
224
  """
232
225
  Algoritmos relacionados:
@@ -0,0 +1,289 @@
1
+ # xss_defense.py
2
+ # GuardianUnivalle_Benito_Yucra/detectores/xss_defense.py
3
+ from __future__ import annotations
4
+ import json
5
+ import logging
6
+ import re
7
+ from typing import List, Tuple, Any, Dict
8
+ from django.conf import settings
9
+ from django.utils.deprecation import MiddlewareMixin
10
+
11
+ # -------------------------------------------------
12
+ # Logger
13
+ # -------------------------------------------------
14
+ logger = logging.getLogger("xssdefense")
15
+ logger.setLevel(logging.INFO)
16
+ if not logger.handlers:
17
+ handler = logging.StreamHandler()
18
+ handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
19
+ logger.addHandler(handler)
20
+
21
+ # -------------------------------------------------
22
+ # Intentar usar bleach (si está instalado). Si no,
23
+ # seguimos con heurísticos de patrones.
24
+ # -------------------------------------------------
25
+ try:
26
+ import bleach
27
+ _BLEACH_AVAILABLE = True
28
+ except Exception:
29
+ _BLEACH_AVAILABLE = False
30
+
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),
47
+ ]
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"])
53
+
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
+
57
+
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
69
+
70
+
71
+ def get_client_ip(request) -> str:
72
+ """
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.
77
+ """
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]:
103
+ """
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>}
108
+ """
109
+ try:
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}
122
+ else:
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}
134
+ except Exception:
135
+ pass
136
+ return {}
137
+
138
+
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, [], []
152
+
153
+ score_total = 0.0
154
+ descripcion = []
155
+ matches = []
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)
163
+
164
+ return round(score_total, 3), descripcion, matches
165
+
166
+
167
+ # -------------------------------------------------
168
+ # Middleware principal XSS
169
+ # -------------------------------------------------
170
+ class XSSDefenseMiddleware(MiddlewareMixin):
171
+ """
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.
177
+ """
178
+
179
+ def process_request(self, request):
180
+ # 1) IP y exclusiones
181
+ client_ip = get_client_ip(request)
182
+ trusted_ips: List[str] = getattr(settings, "XSS_DEFENSE_TRUSTED_IPS", [])
183
+ if client_ip and client_ip in trusted_ips:
184
+ return None
185
+
186
+ excluded_paths: List[str] = getattr(settings, "XSS_DEFENSE_EXCLUDED_PATHS", [])
187
+ if any(request.path.startswith(p) for p in excluded_paths):
188
+ return None
189
+
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:
199
+ return None
200
+
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:
250
+ return None
251
+
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]
256
+
257
+ logger.warning(
258
+ "XSS detectado desde IP %s URL=%s Score=%.3f Desc=%s",
259
+ client_ip,
260
+ url,
261
+ score_rounded,
262
+ all_descriptions,
263
+ )
264
+
265
+ # 6) marcar en el request (AuditoriaMiddleware lo consumirá)
266
+ request.xss_attack_info = {
267
+ "ip": client_ip,
268
+ "tipos": ["XSS"],
269
+ "descripcion": all_descriptions,
270
+ "payload": payload_for_request,
271
+ "score": score_rounded,
272
+ "url": url,
273
+ }
274
+
275
+ # 7) NO bloquear aquí — lo hace AuditoriaMiddleware según su política
276
+ return None
277
+
278
+ # =====================================================
279
+ # === INFORMACIÓN EXTRA ===
280
+ # =====================================================
281
+ """
282
+ Algoritmos relacionados:
283
+ - Se recomienda almacenar los payloads XSS cifrados con AES-GCM
284
+ para confidencialidad e integridad.
285
+
286
+ Contribución a fórmula de amenaza S:
287
+ S_xss = w_xss * detecciones_xss
288
+ Ejemplo: S_xss = 0.3 * 2 = 0.6
289
+ """
@@ -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.44
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
@@ -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.44
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,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "GuardianUnivalle-Benito-Yucra" # usar mayúsculas consistente
7
- version = "0.1.43"
7
+ version = "0.1.44"
8
8
  description = "Middleware y detectores de seguridad (SQLi, XSS, CSRF, DoS, Keylogger) para Django/Flask"
9
9
  authors = [
10
10
  { name = "Andres Benito Calle Yucra", email = "benitoandrescalle035@gmail.com" }
@@ -1,197 +0,0 @@
1
- # xss_defense.py
2
- from __future__ import annotations
3
- import json
4
- import logging
5
- import re
6
- from typing import List, Tuple
7
- from django.conf import settings
8
- from django.utils.deprecation import MiddlewareMixin
9
-
10
- # =====================================================
11
- # === CONFIGURACIÓN LOGGER ===
12
- # =====================================================
13
- logger = logging.getLogger("xssdefense")
14
- logger.setLevel(logging.INFO)
15
- if not logger.handlers:
16
- handler = logging.StreamHandler()
17
- handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
18
- logger.addHandler(handler)
19
-
20
- # =====================================================
21
- # === INTENTAR CARGAR BLEACH ===
22
- # =====================================================
23
- try:
24
- import bleach
25
-
26
- _BLEACH_AVAILABLE = True
27
- except Exception:
28
- _BLEACH_AVAILABLE = False
29
-
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"),
42
- ]
43
-
44
-
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
56
-
57
- for patt, message in XSS_PATTERNS:
58
- if patt.search(text):
59
- matches.append(message)
60
-
61
- return len(matches) > 0, matches
62
-
63
-
64
- def sanitize_input_basic(text: str) -> str:
65
- """
66
- Sanitiza una cadena eliminando etiquetas o caracteres peligrosos.
67
- Usa bleach si está disponible, de lo contrario hace escape manual.
68
- """
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:
90
- """
91
- Extrae un texto combinado con información del cuerpo, querystring,
92
- agente de usuario y referer para análisis XSS.
93
- """
94
- parts: List[str] = []
95
-
96
- 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))
102
- else:
103
- body_text = request.body.decode("utf-8", errors="ignore")
104
- if body_text:
105
- parts.append(body_text)
106
- except Exception:
107
- pass
108
-
109
- qs = request.META.get("QUERY_STRING", "")
110
- if qs:
111
- parts.append(qs)
112
-
113
- parts.append(request.META.get("HTTP_USER_AGENT", ""))
114
- parts.append(request.META.get("HTTP_REFERER", ""))
115
-
116
- return " ".join([p for p in parts if p])
117
-
118
-
119
- # =====================================================
120
- # === MIDDLEWARE DE DEFENSA XSS ===
121
- # =====================================================
122
- class XSSDefenseMiddleware(MiddlewareMixin):
123
- """
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).
128
- """
129
-
130
- def process_request(self, request):
131
- # ---------------------------------------------
132
- # 1. Filtrar IPs de confianza
133
- # ---------------------------------------------
134
- 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:
137
- return None
138
-
139
- # ---------------------------------------------
140
- # 2. Excluir rutas seguras
141
- # ---------------------------------------------
142
- excluded_paths: List[str] = getattr(settings, "XSS_DEFENSE_EXCLUDED_PATHS", [])
143
- if any(request.path.startswith(p) for p in excluded_paths):
144
- return None
145
-
146
- # ---------------------------------------------
147
- # 3. Extraer y analizar payload
148
- # ---------------------------------------------
149
- payload = extract_payload_text(request)
150
- if not payload:
151
- return None
152
-
153
- flagged, matches = detect_xss_text(payload)
154
- if not flagged:
155
- return None
156
-
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
163
-
164
- # ---------------------------------------------
165
- # 5. Loggear y marcar en el request
166
- # ---------------------------------------------
167
- logger.warning(
168
- "XSS detectado desde IP %s: %s ; payload: %.200s ; score: %.2f",
169
- client_ip,
170
- matches,
171
- payload,
172
- s_xss,
173
- )
174
-
175
- request.xss_attack_info = {
176
- "ip": client_ip,
177
- "tipos": ["XSS"],
178
- "descripcion": matches,
179
- "payload": payload,
180
- "score": s_xss,
181
- }
182
-
183
- return None
184
-
185
-
186
- # =====================================================
187
- # === INFORMACIÓN EXTRA ===
188
- # =====================================================
189
- """
190
- Algoritmos relacionados:
191
- - Se recomienda almacenar los payloads XSS cifrados con AES-GCM
192
- para confidencialidad e integridad.
193
-
194
- Contribución a fórmula de amenaza S:
195
- S_xss = w_xss * detecciones_xss
196
- Ejemplo: S_xss = 0.3 * 2 = 0.6
197
- """