GuardianUnivalle-Benito-Yucra 0.1.43__py3-none-any.whl → 0.1.44__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.
- GuardianUnivalle_Benito_Yucra/detectores/detector_csrf.py +34 -41
- GuardianUnivalle_Benito_Yucra/detectores/detector_xss.py +208 -116
- {guardianunivalle_benito_yucra-0.1.43.dist-info → guardianunivalle_benito_yucra-0.1.44.dist-info}/METADATA +1 -1
- {guardianunivalle_benito_yucra-0.1.43.dist-info → guardianunivalle_benito_yucra-0.1.44.dist-info}/RECORD +7 -7
- {guardianunivalle_benito_yucra-0.1.43.dist-info → guardianunivalle_benito_yucra-0.1.44.dist-info}/WHEEL +0 -0
- {guardianunivalle_benito_yucra-0.1.43.dist-info → guardianunivalle_benito_yucra-0.1.44.dist-info}/licenses/LICENSE +0 -0
- {guardianunivalle_benito_yucra-0.1.43.dist-info → guardianunivalle_benito_yucra-0.1.44.dist-info}/top_level.txt +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"
|
|
71
|
-
re.compile(r"
|
|
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
|
|
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
|
|
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
|
-
-
|
|
164
|
-
- No bloquea
|
|
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
|
-
#
|
|
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:
|
|
@@ -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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
(re.compile(r"
|
|
37
|
-
(re.compile(r"
|
|
38
|
-
(re.compile(r"<\s*
|
|
39
|
-
(re.compile(r"<\s*
|
|
40
|
-
(re.compile(r"
|
|
41
|
-
(re.compile(r"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
71
|
+
def get_client_ip(request) -> str:
|
|
65
72
|
"""
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
(
|
|
80
|
-
(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
|
92
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
125
|
-
-
|
|
126
|
-
-
|
|
127
|
-
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
258
|
+
"XSS detectado desde IP %s URL=%s Score=%.3f Desc=%s",
|
|
169
259
|
client_ip,
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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":
|
|
179
|
-
"payload":
|
|
180
|
-
"score":
|
|
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.
|
|
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,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=
|
|
7
|
+
GuardianUnivalle_Benito_Yucra/detectores/detector_csrf.py,sha256=tEFJTydl9gKcYQPxeQNSQ1WwoY_4oHvtDrhJROeH4E0,8109
|
|
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=
|
|
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.
|
|
17
|
-
guardianunivalle_benito_yucra-0.1.
|
|
18
|
-
guardianunivalle_benito_yucra-0.1.
|
|
19
|
-
guardianunivalle_benito_yucra-0.1.
|
|
20
|
-
guardianunivalle_benito_yucra-0.1.
|
|
16
|
+
guardianunivalle_benito_yucra-0.1.44.dist-info/licenses/LICENSE,sha256=5e4IdL542v1E8Ft0A24GZjrxZeTsVK7XrS3mZEUhPtM,37
|
|
17
|
+
guardianunivalle_benito_yucra-0.1.44.dist-info/METADATA,sha256=8h01QKIbaxCdYkJ9wPEkmiIsRRDlIptoO_O_aWJkoaQ,1893
|
|
18
|
+
guardianunivalle_benito_yucra-0.1.44.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
19
|
+
guardianunivalle_benito_yucra-0.1.44.dist-info/top_level.txt,sha256=HTWfZM64WAV_QYr5cnXnLuabQt92dvlxqlR3pCwpbDQ,30
|
|
20
|
+
guardianunivalle_benito_yucra-0.1.44.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|