GuardianUnivalle-Benito-Yucra 0.1.43__tar.gz → 0.1.45__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.
- {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.45}/GuardianUnivalle_Benito_Yucra/detectores/detector_csrf.py +52 -88
- guardianunivalle_benito_yucra-0.1.45/GuardianUnivalle_Benito_Yucra/detectores/detector_xss.py +289 -0
- {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.45}/GuardianUnivalle_Benito_Yucra.egg-info/PKG-INFO +1 -1
- {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.45}/PKG-INFO +1 -1
- {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.45}/pyproject.toml +1 -1
- guardianunivalle_benito_yucra-0.1.43/GuardianUnivalle_Benito_Yucra/detectores/detector_xss.py +0 -197
- {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.45}/GuardianUnivalle_Benito_Yucra/__init__.py +0 -0
- {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.45}/GuardianUnivalle_Benito_Yucra/auditoria/registro_auditoria.py +0 -0
- {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.45}/GuardianUnivalle_Benito_Yucra/criptografia/cifrado_aead.py +0 -0
- {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.45}/GuardianUnivalle_Benito_Yucra/criptografia/intercambio_claves.py +0 -0
- {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.45}/GuardianUnivalle_Benito_Yucra/criptografia/kdf.py +0 -0
- {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.45}/GuardianUnivalle_Benito_Yucra/detectores/detector_dos.py +0 -0
- {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.45}/GuardianUnivalle_Benito_Yucra/detectores/detector_keylogger.py +0 -0
- {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.45}/GuardianUnivalle_Benito_Yucra/detectores/detector_sql.py +0 -0
- {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.45}/GuardianUnivalle_Benito_Yucra/middleware_web/middleware_web.py +0 -0
- {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.45}/GuardianUnivalle_Benito_Yucra/mitigacion/limitador_peticion.py +0 -0
- {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.45}/GuardianUnivalle_Benito_Yucra/mitigacion/lista_bloqueo.py +0 -0
- {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.45}/GuardianUnivalle_Benito_Yucra/puntuacion/puntuacion_amenaza.py +0 -0
- {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.45}/GuardianUnivalle_Benito_Yucra/utilidades.py +0 -0
- {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.45}/GuardianUnivalle_Benito_Yucra.egg-info/SOURCES.txt +0 -0
- {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.45}/GuardianUnivalle_Benito_Yucra.egg-info/dependency_links.txt +0 -0
- {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.45}/GuardianUnivalle_Benito_Yucra.egg-info/requires.txt +0 -0
- {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.45}/GuardianUnivalle_Benito_Yucra.egg-info/top_level.txt +0 -0
- {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.45}/LICENSE +0 -0
- {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.45}/README.md +0 -0
- {guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.45}/setup.cfg +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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"
|
|
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
|
|
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 ;
|
|
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:
|
|
@@ -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.
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: GuardianUnivalle-Benito-Yucra
|
|
3
|
-
Version: 0.1.
|
|
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
|
{guardianunivalle_benito_yucra-0.1.43 → guardianunivalle_benito_yucra-0.1.45}/pyproject.toml
RENAMED
|
@@ -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.
|
|
7
|
+
version = "0.1.45"
|
|
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" }
|
guardianunivalle_benito_yucra-0.1.43/GuardianUnivalle_Benito_Yucra/detectores/detector_xss.py
DELETED
|
@@ -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
|
-
("&", "&"),
|
|
77
|
-
("<", "<"),
|
|
78
|
-
(">", ">"),
|
|
79
|
-
('"', """),
|
|
80
|
-
("'", "'"),
|
|
81
|
-
("/", "/"),
|
|
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
|
-
"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|