GuardianUnivalle-Benito-Yucra 0.1.69__tar.gz → 0.1.71__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.69 → guardianunivalle_benito_yucra-0.1.71}/GuardianUnivalle_Benito_Yucra/detectores/detector_csrf.py +111 -20
- {guardianunivalle_benito_yucra-0.1.69 → guardianunivalle_benito_yucra-0.1.71}/GuardianUnivalle_Benito_Yucra/detectores/detector_sql.py +86 -39
- {guardianunivalle_benito_yucra-0.1.69 → guardianunivalle_benito_yucra-0.1.71}/GuardianUnivalle_Benito_Yucra/detectores/detector_xss.py +34 -41
- guardianunivalle_benito_yucra-0.1.71/GuardianUnivalle_Benito_Yucra.egg-info/PKG-INFO +265 -0
- guardianunivalle_benito_yucra-0.1.71/PKG-INFO +265 -0
- guardianunivalle_benito_yucra-0.1.71/README.md +242 -0
- {guardianunivalle_benito_yucra-0.1.69 → guardianunivalle_benito_yucra-0.1.71}/pyproject.toml +1 -1
- guardianunivalle_benito_yucra-0.1.69/GuardianUnivalle_Benito_Yucra.egg-info/PKG-INFO +0 -192
- guardianunivalle_benito_yucra-0.1.69/PKG-INFO +0 -192
- guardianunivalle_benito_yucra-0.1.69/README.md +0 -169
- {guardianunivalle_benito_yucra-0.1.69 → guardianunivalle_benito_yucra-0.1.71}/GuardianUnivalle_Benito_Yucra/__init__.py +0 -0
- {guardianunivalle_benito_yucra-0.1.69 → guardianunivalle_benito_yucra-0.1.71}/GuardianUnivalle_Benito_Yucra/auditoria/registro_auditoria.py +0 -0
- {guardianunivalle_benito_yucra-0.1.69 → guardianunivalle_benito_yucra-0.1.71}/GuardianUnivalle_Benito_Yucra/criptografia/cifrado_aead.py +0 -0
- {guardianunivalle_benito_yucra-0.1.69 → guardianunivalle_benito_yucra-0.1.71}/GuardianUnivalle_Benito_Yucra/criptografia/intercambio_claves.py +0 -0
- {guardianunivalle_benito_yucra-0.1.69 → guardianunivalle_benito_yucra-0.1.71}/GuardianUnivalle_Benito_Yucra/criptografia/kdf.py +0 -0
- {guardianunivalle_benito_yucra-0.1.69 → guardianunivalle_benito_yucra-0.1.71}/GuardianUnivalle_Benito_Yucra/detectores/detector_dos.py +0 -0
- {guardianunivalle_benito_yucra-0.1.69 → guardianunivalle_benito_yucra-0.1.71}/GuardianUnivalle_Benito_Yucra/middleware_web/middleware_web.py +0 -0
- {guardianunivalle_benito_yucra-0.1.69 → guardianunivalle_benito_yucra-0.1.71}/GuardianUnivalle_Benito_Yucra/mitigacion/limitador_peticion.py +0 -0
- {guardianunivalle_benito_yucra-0.1.69 → guardianunivalle_benito_yucra-0.1.71}/GuardianUnivalle_Benito_Yucra/mitigacion/lista_bloqueo.py +0 -0
- {guardianunivalle_benito_yucra-0.1.69 → guardianunivalle_benito_yucra-0.1.71}/GuardianUnivalle_Benito_Yucra/puntuacion/puntuacion_amenaza.py +0 -0
- {guardianunivalle_benito_yucra-0.1.69 → guardianunivalle_benito_yucra-0.1.71}/GuardianUnivalle_Benito_Yucra/utilidades.py +0 -0
- {guardianunivalle_benito_yucra-0.1.69 → guardianunivalle_benito_yucra-0.1.71}/GuardianUnivalle_Benito_Yucra.egg-info/SOURCES.txt +0 -0
- {guardianunivalle_benito_yucra-0.1.69 → guardianunivalle_benito_yucra-0.1.71}/GuardianUnivalle_Benito_Yucra.egg-info/dependency_links.txt +0 -0
- {guardianunivalle_benito_yucra-0.1.69 → guardianunivalle_benito_yucra-0.1.71}/GuardianUnivalle_Benito_Yucra.egg-info/requires.txt +0 -0
- {guardianunivalle_benito_yucra-0.1.69 → guardianunivalle_benito_yucra-0.1.71}/GuardianUnivalle_Benito_Yucra.egg-info/top_level.txt +0 -0
- {guardianunivalle_benito_yucra-0.1.69 → guardianunivalle_benito_yucra-0.1.71}/LICENSE +0 -0
- {guardianunivalle_benito_yucra-0.1.69 → guardianunivalle_benito_yucra-0.1.71}/setup.cfg +0 -0
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
#
|
|
1
|
+
# csrf_defense.py
|
|
2
|
+
# GuardianUnivalle_Benito_Yucra/detectores/csrf_defense.py
|
|
3
|
+
|
|
2
4
|
from __future__ import annotations
|
|
3
5
|
import secrets
|
|
4
6
|
import logging
|
|
5
7
|
import re
|
|
6
8
|
import json
|
|
7
|
-
from typing import List
|
|
9
|
+
from typing import List, Dict, Any
|
|
8
10
|
from urllib.parse import urlparse
|
|
9
11
|
from django.conf import settings
|
|
10
12
|
from django.utils.deprecation import MiddlewareMixin
|
|
@@ -21,21 +23,42 @@ CSRF_HEADER_NAMES = ("HTTP_X_CSRFTOKEN", "HTTP_X_CSRF_TOKEN")
|
|
|
21
23
|
CSRF_COOKIE_NAME = getattr(settings, "CSRF_COOKIE_NAME", "csrftoken")
|
|
22
24
|
POST_FIELD_NAME = "csrfmiddlewaretoken"
|
|
23
25
|
|
|
24
|
-
# Patrón de Content-Type sospechoso
|
|
26
|
+
# Patrón de Content-Type sospechoso - EXPANDIDO
|
|
25
27
|
SUSPICIOUS_CT_PATTERNS = [
|
|
26
28
|
re.compile(r"text/plain", re.I),
|
|
27
29
|
re.compile(r"application/x-www-form-urlencoded", re.I),
|
|
28
30
|
re.compile(r"multipart/form-data", re.I),
|
|
31
|
+
re.compile(r"application/json", re.I),
|
|
32
|
+
re.compile(r"text/html", re.I), # Agregado para HTML CSRF
|
|
29
33
|
]
|
|
30
34
|
|
|
31
|
-
# Parámetros sensibles típicos de CSRF
|
|
35
|
+
# Parámetros sensibles típicos de CSRF - EXPANDIDO
|
|
32
36
|
SENSITIVE_PARAMS = [
|
|
33
|
-
"password", "csrfmiddlewaretoken", "token", "amount", "transfer", "delete", "update"
|
|
37
|
+
"password", "csrfmiddlewaretoken", "token", "amount", "transfer", "delete", "update", "action", "email", "username"
|
|
34
38
|
]
|
|
35
39
|
|
|
40
|
+
# Campos sensibles: ANALIZAMOS COMPLETAMENTE SIN DESCUENTO PARA ROBUSTEZ MÁXIMA
|
|
41
|
+
SENSITIVE_FIELDS = ["password", "csrfmiddlewaretoken", "token", "auth", "email", "username"]
|
|
42
|
+
|
|
36
43
|
CSRF_DEFENSE_MIN_SIGNALS = getattr(settings, "CSRF_DEFENSE_MIN_SIGNALS", 1)
|
|
37
44
|
CSRF_DEFENSE_EXCLUDED_API_PREFIXES = getattr(settings, "CSRF_DEFENSE_EXCLUDED_API_PREFIXES", ["/api/"])
|
|
38
45
|
|
|
46
|
+
# PATRONES EXPANDIDOS PARA ANÁLISIS DE PAYLOAD EN TODOS LOS CAMPOS (SIN DESCUENTO)
|
|
47
|
+
CSRF_PAYLOAD_PATTERNS = [
|
|
48
|
+
(re.compile(r"<script[^>]*>.*?</script>", re.I | re.S), "Script tag en payload", 0.9),
|
|
49
|
+
(re.compile(r"javascript\s*:", re.I), "URI javascript: en payload", 0.8),
|
|
50
|
+
(re.compile(r"http[s]?://[^\s]+", re.I), "URL externa en payload", 0.7),
|
|
51
|
+
(re.compile(r"eval\s*\(", re.I), "eval() en payload", 1.0),
|
|
52
|
+
(re.compile(r"document\.cookie", re.I), "Acceso a cookie en payload", 0.9),
|
|
53
|
+
(re.compile(r"innerHTML\s*=", re.I), "Manipulación DOM innerHTML", 0.8),
|
|
54
|
+
(re.compile(r"XMLHttpRequest", re.I), "XHR en payload", 0.7),
|
|
55
|
+
(re.compile(r"fetch\s*\(", re.I), "fetch() en payload", 0.7),
|
|
56
|
+
(re.compile(r"&#x[0-9a-fA-F]+;", re.I), "Entidades HTML en payload", 0.6),
|
|
57
|
+
(re.compile(r"%3Cscript", re.I), "Script URL-encoded en payload", 0.8),
|
|
58
|
+
(re.compile(r"on\w+\s*=", re.I), "Eventos on* en payload", 0.7),
|
|
59
|
+
(re.compile(r"alert\s*\(", re.I), "alert() en payload (prueba)", 0.5),
|
|
60
|
+
]
|
|
61
|
+
|
|
39
62
|
def get_client_ip(request):
|
|
40
63
|
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
|
|
41
64
|
if x_forwarded_for:
|
|
@@ -62,15 +85,15 @@ def origin_matches_host(request) -> bool:
|
|
|
62
85
|
host = host_header.split(":")[0]
|
|
63
86
|
origin = request.META.get("HTTP_ORIGIN", "")
|
|
64
87
|
referer = request.META.get("HTTP_REFERER", "")
|
|
65
|
-
origin_host = host_from_header(origin)
|
|
66
|
-
referer_host = host_from_header(referer)
|
|
67
88
|
# Bloquear obvious javascript: referers
|
|
68
89
|
if any(re.search(r"(javascript:|<script|data:text/html)", h or "", re.I) for h in [origin, referer]):
|
|
69
90
|
return False
|
|
70
|
-
if origin_host
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
91
|
+
if origin_host := host_from_header(origin):
|
|
92
|
+
if origin_host == host:
|
|
93
|
+
return True
|
|
94
|
+
if referer_host := host_from_header(referer):
|
|
95
|
+
if referer_host == host:
|
|
96
|
+
return True
|
|
74
97
|
if not origin and not referer:
|
|
75
98
|
return True
|
|
76
99
|
return False
|
|
@@ -118,6 +141,34 @@ def extract_parameters(request) -> List[str]:
|
|
|
118
141
|
pass
|
|
119
142
|
return params
|
|
120
143
|
|
|
144
|
+
# FUNCIÓN ROBUSTA: Analizar payload en TODOS los campos (incluyendo sensibles sin descuento)
|
|
145
|
+
def analyze_payload(value: str) -> float:
|
|
146
|
+
score = 0.0
|
|
147
|
+
for patt, desc, weight in CSRF_PAYLOAD_PATTERNS:
|
|
148
|
+
if patt.search(value):
|
|
149
|
+
score += weight # Score full, sin descuento
|
|
150
|
+
return round(score, 3)
|
|
151
|
+
|
|
152
|
+
# NUEVA FUNCIÓN: Extraer y analizar query string
|
|
153
|
+
def analyze_query_string(request) -> float:
|
|
154
|
+
qs = request.META.get("QUERY_STRING", "")
|
|
155
|
+
if qs:
|
|
156
|
+
return analyze_payload(qs)
|
|
157
|
+
return 0.0
|
|
158
|
+
|
|
159
|
+
# NUEVA FUNCIÓN: Analizar headers adicionales
|
|
160
|
+
def analyze_headers(request) -> List[str]:
|
|
161
|
+
issues = []
|
|
162
|
+
ua = request.META.get("HTTP_USER_AGENT", "")
|
|
163
|
+
if re.search(r"(script|<|eval|bot|crawler)", ua, re.I):
|
|
164
|
+
issues.append("User-Agent sospechoso (posible automatización/bot)")
|
|
165
|
+
|
|
166
|
+
accept_lang = request.META.get("HTTP_ACCEPT_LANGUAGE", "")
|
|
167
|
+
if not accept_lang or len(accept_lang) < 2:
|
|
168
|
+
issues.append("Accept-Language ausente o muy corto (posible bot)")
|
|
169
|
+
|
|
170
|
+
return issues
|
|
171
|
+
|
|
121
172
|
class CSRFDefenseMiddleware(MiddlewareMixin):
|
|
122
173
|
def process_request(self, request):
|
|
123
174
|
# Excluir APIs JSON si se configuró así
|
|
@@ -136,7 +187,7 @@ class CSRFDefenseMiddleware(MiddlewareMixin):
|
|
|
136
187
|
return None
|
|
137
188
|
|
|
138
189
|
method = (request.method or "").upper()
|
|
139
|
-
if method not in STATE_CHANGING_METHODS:
|
|
190
|
+
if method not in STATE_CHANGING_METHODS: # CORREGIDO: Agregado "in"
|
|
140
191
|
return None
|
|
141
192
|
|
|
142
193
|
descripcion: List[str] = []
|
|
@@ -174,33 +225,73 @@ class CSRFDefenseMiddleware(MiddlewareMixin):
|
|
|
174
225
|
if origin and host_from_header(origin) != (request.META.get("HTTP_HOST") or "").split(":")[0]:
|
|
175
226
|
descripcion.append("JSON POST desde origen externo (posible CSRF)")
|
|
176
227
|
|
|
228
|
+
# 7) Análisis ROBUSTO de payload en TODOS los campos (sin descuento)
|
|
229
|
+
payload_score = 0.0
|
|
230
|
+
payload_summary: List[Dict[str, Any]] = []
|
|
231
|
+
try:
|
|
232
|
+
# Analizar POST
|
|
233
|
+
if hasattr(request, "POST"):
|
|
234
|
+
for key, value in request.POST.items():
|
|
235
|
+
if isinstance(value, str):
|
|
236
|
+
score = analyze_payload(value)
|
|
237
|
+
payload_score += score
|
|
238
|
+
if score > 0:
|
|
239
|
+
payload_summary.append({"field": key, "snippet": value[:300], "score": score})
|
|
240
|
+
# Analizar JSON
|
|
241
|
+
if "application/json" in content_type:
|
|
242
|
+
data = json.loads(request.body.decode("utf-8") or "{}")
|
|
243
|
+
for key, value in data.items():
|
|
244
|
+
if isinstance(value, str):
|
|
245
|
+
score = analyze_payload(value)
|
|
246
|
+
payload_score += score
|
|
247
|
+
if score > 0:
|
|
248
|
+
payload_summary.append({"field": key, "snippet": value[:300], "score": score})
|
|
249
|
+
except Exception as e:
|
|
250
|
+
logger.debug(f"Error analizando payload: {e}")
|
|
251
|
+
|
|
252
|
+
if payload_score > 0:
|
|
253
|
+
descripcion.append(f"Payload sospechoso detectado (score total: {payload_score})")
|
|
254
|
+
|
|
255
|
+
# 8) Análisis de query string
|
|
256
|
+
qs_score = analyze_query_string(request)
|
|
257
|
+
if qs_score > 0:
|
|
258
|
+
descripcion.append(f"Query string sospechosa (score: {qs_score})")
|
|
259
|
+
payload_score += qs_score
|
|
260
|
+
|
|
261
|
+
# 9) Análisis de headers adicionales
|
|
262
|
+
header_issues = analyze_headers(request)
|
|
263
|
+
descripcion.extend(header_issues)
|
|
264
|
+
|
|
177
265
|
# Señales >= umbral => marcar
|
|
178
|
-
|
|
266
|
+
total_signals = len(descripcion)
|
|
267
|
+
if descripcion and total_signals >= CSRF_DEFENSE_MIN_SIGNALS:
|
|
179
268
|
w_csrf = getattr(settings, "CSRF_DEFENSE_WEIGHT", 0.2)
|
|
180
|
-
s_csrf = w_csrf *
|
|
269
|
+
s_csrf = w_csrf * total_signals + payload_score # Score full sin descuento
|
|
181
270
|
request.csrf_attack_info = {
|
|
182
271
|
"ip": client_ip,
|
|
183
272
|
"tipos": ["CSRF"],
|
|
184
273
|
"descripcion": descripcion,
|
|
185
|
-
"payload":
|
|
274
|
+
"payload": json.dumps(payload_summary, ensure_ascii=False)[:1000],
|
|
186
275
|
"score": s_csrf,
|
|
187
276
|
}
|
|
188
277
|
logger.warning(
|
|
189
|
-
"CSRF detectado desde IP %s: %s ; path=%s ; Content-Type=%s ; score=%.2f",
|
|
278
|
+
"CSRF detectado desde IP %s: %s ; path=%s ; Content-Type=%s ; score=%.2f (Ultra-Robust: nada ignorado)",
|
|
190
279
|
client_ip, descripcion, request.path, content_type, s_csrf
|
|
191
280
|
)
|
|
192
281
|
else:
|
|
193
282
|
if descripcion:
|
|
194
|
-
logger.debug(f"[CSRFDefense] low-signals ({
|
|
283
|
+
logger.debug(f"[CSRFDefense] low-signals ({total_signals}) not marking: {descripcion}")
|
|
195
284
|
|
|
196
285
|
return None
|
|
197
286
|
|
|
198
287
|
"""
|
|
199
|
-
CSRF Defense Middleware -
|
|
200
|
-
|
|
288
|
+
CSRF Defense Middleware - Ultra-Robusto (Nada Ignorado)
|
|
289
|
+
=======================================================
|
|
201
290
|
- Detecta múltiples categorías de CSRF: clásico, login, logout, password change, file/action, JSON API.
|
|
202
|
-
- Escanea payloads POST, GET y
|
|
291
|
+
- Escanea payloads POST, GET, JSON, query string y headers, incluyendo TODOS los campos (sensibles y no) con score full (sin descuento).
|
|
203
292
|
- Detecta parámetros sensibles enviados en GET o JSON desde origen externo.
|
|
293
|
+
- Análisis adicional de User-Agent, Accept-Language, y payloads con patrones expandidos.
|
|
204
294
|
- Scoring configurable y logging detallado.
|
|
205
295
|
- Fácil integración con auditoría XSS/SQLi.
|
|
296
|
+
- Máxima robustez: no ignora nada para detección óptima.
|
|
206
297
|
"""
|
|
@@ -8,7 +8,7 @@ from django.utils.deprecation import MiddlewareMixin
|
|
|
8
8
|
from django.conf import settings
|
|
9
9
|
import urllib.parse
|
|
10
10
|
import html
|
|
11
|
-
from typing import List, Tuple, Dict
|
|
11
|
+
from typing import List, Tuple, Dict, Any
|
|
12
12
|
|
|
13
13
|
# ----------------------------
|
|
14
14
|
# Configuración del logger
|
|
@@ -108,9 +108,23 @@ SQL_PATTERNS: List[Tuple[re.Pattern, str, float]] = [
|
|
|
108
108
|
# ------------------ Catch‑all aggressive patterns (usar con cuidado) ------------------
|
|
109
109
|
(re.compile(r"(['\"]).*?;\s*(drop|truncate|delete|update|insert)\b", re.I | re.S), "Cadena con terminador y DDL/DML (potencial ataque)", 0.9),
|
|
110
110
|
(re.compile(r"\b(or)\b\s+1\s*=\s*1\b", re.I), "OR 1=1 tautology", 0.85),
|
|
111
|
+
|
|
112
|
+
# ---------- NUEVOS PATRONES PARA ROBUSTEZ ----------
|
|
113
|
+
(re.compile(r"\b(select\s+.*\s+from\s+.*\s+where\s+.*\s+in\s*\()", re.I | re.S), "Subquery anidada (IN subquery)", 0.75),
|
|
114
|
+
(re.compile(r"\bcase\s+when\s+.*\s+then\s+.*\s+else\b", re.I), "CASE WHEN (blind boolean)", 0.78),
|
|
115
|
+
(re.compile(r"/\*\!.+\*\//", re.I), "Comentarios condicionales MySQL (/*!...*/)", 0.7),
|
|
116
|
+
(re.compile(r"\bif\s*\(\s*.*\s*,\s*.*\s*,\s*.*\s*\)", re.I), "IF() MySQL (conditional)", 0.72),
|
|
117
|
+
(re.compile(r"\bgroup_concat\s*\(", re.I), "GROUP_CONCAT() (exfiltración en error)", 0.8),
|
|
111
118
|
]
|
|
112
119
|
|
|
113
|
-
|
|
120
|
+
# Campos sensibles: ANALIZAMOS COMPLETAMENTE SIN DESCUENTO PARA ROBUSTEZ MÁXIMA
|
|
121
|
+
SENSITIVE_FIELDS = ["password", "csrfmiddlewaretoken", "token", "auth", "email", "username"]
|
|
122
|
+
|
|
123
|
+
DEFAULT_THRESHOLDS = {
|
|
124
|
+
"HIGH": 1.8,
|
|
125
|
+
"MEDIUM": 1.0,
|
|
126
|
+
"LOW": 0.5,
|
|
127
|
+
}
|
|
114
128
|
|
|
115
129
|
# ----------------------------
|
|
116
130
|
# Obtener IP real del cliente
|
|
@@ -124,26 +138,34 @@ def get_client_ip(request):
|
|
|
124
138
|
return request.META.get("REMOTE_ADDR", "")
|
|
125
139
|
|
|
126
140
|
# ----------------------------
|
|
127
|
-
# Extraer payload de la solicitud
|
|
141
|
+
# Extraer payload de la solicitud - MEJORADO PARA ANÁLISIS POR CAMPO
|
|
128
142
|
# ----------------------------
|
|
129
|
-
def
|
|
130
|
-
parts = []
|
|
143
|
+
def extract_payload_as_map(request) -> Dict[str, Any]:
|
|
131
144
|
try:
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
145
|
+
ct = request.META.get("CONTENT_TYPE", "")
|
|
146
|
+
if "application/json" in ct:
|
|
147
|
+
raw = request.body.decode("utf-8") or "{}"
|
|
148
|
+
try:
|
|
149
|
+
data = json.loads(raw)
|
|
150
|
+
if isinstance(data, dict):
|
|
151
|
+
return data
|
|
152
|
+
else:
|
|
153
|
+
return {"raw": raw}
|
|
154
|
+
except Exception:
|
|
155
|
+
return {"raw": raw}
|
|
135
156
|
else:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
157
|
+
try:
|
|
158
|
+
post = request.POST.dict()
|
|
159
|
+
if post:
|
|
160
|
+
return post
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
raw = request.body.decode("utf-8", errors="ignore")
|
|
164
|
+
if raw:
|
|
165
|
+
return {"raw": raw}
|
|
139
166
|
except Exception:
|
|
140
167
|
pass
|
|
141
|
-
|
|
142
|
-
qs = request.META.get("QUERY_STRING", "")
|
|
143
|
-
if qs:
|
|
144
|
-
parts.append(qs)
|
|
145
|
-
|
|
146
|
-
return " ".join(parts)
|
|
168
|
+
return {}
|
|
147
169
|
|
|
148
170
|
# ----------------------------
|
|
149
171
|
# Normalización / preprocesamiento
|
|
@@ -159,13 +181,12 @@ def normalize_input(s: str) -> str:
|
|
|
159
181
|
s_dec = html.unescape(s_dec)
|
|
160
182
|
except Exception:
|
|
161
183
|
pass
|
|
162
|
-
# Reemplazo seguro de secuencias \xNN
|
|
163
184
|
s_dec = re.sub(r"\\x([0-9a-fA-F]{2})", r"\\x\g<1>", s_dec)
|
|
164
185
|
s_dec = re.sub(r"\s+", " ", s_dec)
|
|
165
186
|
return s_dec.strip()
|
|
166
187
|
|
|
167
188
|
# ----------------------------
|
|
168
|
-
# Detector SQLi
|
|
189
|
+
# Detector SQLi - ROBUSTO SIN DESCUENTO
|
|
169
190
|
# ----------------------------
|
|
170
191
|
def detect_sql_injection(text: str) -> Dict:
|
|
171
192
|
norm = normalize_input(text or "")
|
|
@@ -174,7 +195,7 @@ def detect_sql_injection(text: str) -> Dict:
|
|
|
174
195
|
descriptions = []
|
|
175
196
|
for pattern, desc, weight in SQL_PATTERNS:
|
|
176
197
|
if pattern.search(norm):
|
|
177
|
-
score += weight
|
|
198
|
+
score += weight # Score full, sin descuento
|
|
178
199
|
matches.append((desc, pattern.pattern, weight))
|
|
179
200
|
descriptions.append(desc)
|
|
180
201
|
|
|
@@ -185,14 +206,8 @@ def detect_sql_injection(text: str) -> Dict:
|
|
|
185
206
|
"sample": norm[:1200],
|
|
186
207
|
}
|
|
187
208
|
|
|
188
|
-
DEFAULT_THRESHOLDS = {
|
|
189
|
-
"HIGH": 1.8,
|
|
190
|
-
"MEDIUM": 1.0,
|
|
191
|
-
"LOW": 0.5,
|
|
192
|
-
}
|
|
193
|
-
|
|
194
209
|
# ----------------------------
|
|
195
|
-
# Middleware SQLi
|
|
210
|
+
# Middleware SQLi - ULTRA-ROBUSTO
|
|
196
211
|
# ----------------------------
|
|
197
212
|
class SQLIDefenseMiddleware(MiddlewareMixin):
|
|
198
213
|
def process_request(self, request):
|
|
@@ -202,33 +217,65 @@ class SQLIDefenseMiddleware(MiddlewareMixin):
|
|
|
202
217
|
|
|
203
218
|
if client_ip in trusted_ips:
|
|
204
219
|
return None
|
|
205
|
-
|
|
206
220
|
referer = request.META.get("HTTP_REFERER", "")
|
|
207
221
|
host = request.get_host()
|
|
208
222
|
if any(url in referer for url in trusted_urls) or any(url in host for url in trusted_urls):
|
|
209
223
|
return None
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
224
|
+
# Extraer datos como mapa para análisis por campo
|
|
225
|
+
data = extract_payload_as_map(request)
|
|
226
|
+
qs = request.META.get("QUERY_STRING", "")
|
|
227
|
+
if qs:
|
|
228
|
+
data["_query_string"] = qs
|
|
229
|
+
if not data:
|
|
230
|
+
return None
|
|
231
|
+
total_score = 0.0
|
|
232
|
+
all_descriptions = []
|
|
233
|
+
all_matches = []
|
|
234
|
+
payload_summary = []
|
|
235
|
+
# Analizar campo por campo - AHORA SIN DESCUENTO PARA ROBUSTEZ
|
|
236
|
+
if isinstance(data, dict):
|
|
237
|
+
for key, value in data.items():
|
|
238
|
+
if isinstance(value, (dict, list)):
|
|
239
|
+
try:
|
|
240
|
+
vtext = json.dumps(value, ensure_ascii=False)
|
|
241
|
+
except Exception:
|
|
242
|
+
vtext = str(value)
|
|
243
|
+
else:
|
|
244
|
+
vtext = str(value or "")
|
|
245
|
+
|
|
246
|
+
result = detect_sql_injection(vtext)
|
|
247
|
+
total_score += result["score"]
|
|
248
|
+
all_descriptions.extend(result["descriptions"])
|
|
249
|
+
all_matches.extend(result["matches"])
|
|
250
|
+
|
|
251
|
+
if result["score"] > 0:
|
|
252
|
+
is_sensitive = isinstance(key, str) and key.lower() in SENSITIVE_FIELDS
|
|
253
|
+
payload_summary.append({"field": key, "snippet": vtext[:300], "sensitive": is_sensitive})
|
|
254
|
+
else:
|
|
255
|
+
raw = str(data)
|
|
256
|
+
result = detect_sql_injection(raw)
|
|
257
|
+
total_score += result["score"]
|
|
258
|
+
all_descriptions.extend(result["descriptions"])
|
|
259
|
+
all_matches.extend(result["matches"])
|
|
260
|
+
if result["score"] > 0:
|
|
261
|
+
payload_summary.append({"field": "raw", "snippet": raw[:500], "sensitive": False})
|
|
262
|
+
|
|
263
|
+
if total_score == 0:
|
|
217
264
|
return None
|
|
218
265
|
|
|
219
266
|
# Registrar ataque
|
|
220
267
|
logger.warning(
|
|
221
268
|
f"[SQLiDetect] IP={client_ip} Host={host} Referer={referer} "
|
|
222
|
-
f"Score={
|
|
269
|
+
f"Score={total_score:.2f} Desc={all_descriptions} Payload={json.dumps(payload_summary, ensure_ascii=False)[:500]}"
|
|
223
270
|
)
|
|
224
271
|
|
|
225
272
|
# Guardar info en request
|
|
226
273
|
request.sql_attack_info = {
|
|
227
274
|
"ip": client_ip,
|
|
228
275
|
"tipos": ["SQLi"],
|
|
229
|
-
"descripcion":
|
|
230
|
-
"payload":
|
|
231
|
-
"score": round(
|
|
276
|
+
"descripcion": all_descriptions,
|
|
277
|
+
"payload": json.dumps(payload_summary, ensure_ascii=False)[:1000],
|
|
278
|
+
"score": round(total_score, 2),
|
|
232
279
|
"url": request.build_absolute_uri(),
|
|
233
280
|
}
|
|
234
281
|
|
|
@@ -29,11 +29,10 @@ except Exception:
|
|
|
29
29
|
_BLEACH_AVAILABLE = False
|
|
30
30
|
|
|
31
31
|
# -------------------------------------------------
|
|
32
|
-
# Patrones XSS con peso (descripcion, peso)
|
|
32
|
+
# Patrones XSS con peso (descripcion, peso) - EXPANDIDOS PARA ROBUSTEZ
|
|
33
33
|
# - pesos mayores = más severo (por ejemplo <script> o javascript:)
|
|
34
|
-
# -
|
|
34
|
+
# - Agregados patrones para DOM-based, polyglots y evasiones avanzadas
|
|
35
35
|
# -------------------------------------------------
|
|
36
|
-
|
|
37
36
|
XSS_PATTERNS: List[Tuple[re.Pattern, str, float]] = [
|
|
38
37
|
# ---------- Máxima severidad / ejecución directa ----------
|
|
39
38
|
(re.compile(r"<\s*script\b", re.I), "Etiqueta <script> (directa)", 0.95),
|
|
@@ -94,20 +93,30 @@ XSS_PATTERNS: List[Tuple[re.Pattern, str, float]] = [
|
|
|
94
93
|
# ---------- Heurísticos de baja severidad (informativo) ----------
|
|
95
94
|
(re.compile(r"<\s*form\b", re.I), "Form (posible vector de ataque relacionado)", 0.25),
|
|
96
95
|
(re.compile(r"(onmouseover|onfocus|onmouseenter|onmouseleave)\b", re.I), "Eventos UI (mouseover/focus)", 0.45),
|
|
96
|
+
|
|
97
|
+
# ---------- NUEVOS PATRONES PARA ROBUSTEZ ----------
|
|
98
|
+
(re.compile(r"\binnerHTML\s*=\s*.*[<>\"']", re.I), "Asignación a innerHTML con tags", 0.85), # DOM-based
|
|
99
|
+
(re.compile(r"\bdocument\.getElementById\s*\(\s*.*\)\.innerHTML", re.I), "Manipulación DOM innerHTML", 0.80),
|
|
100
|
+
(re.compile(r"<script[^>]*src\s*=\s*['\"][^'\"]*['\"][^>]*>", re.I), "Script externo (posible carga remota)", 0.75),
|
|
101
|
+
(re.compile(r"\bXMLHttpRequest\s*\(\s*\)\.open\s*\(\s*['\"](GET|POST)['\"]", re.I), "XHR manipulado (posible exfiltración)", 0.70),
|
|
102
|
+
(re.compile(r"<\s*link\b[^>]*\bhref\s*=\s*['\"][^'\"]*javascript\s*:", re.I), "Link con href javascript:", 0.78),
|
|
103
|
+
(re.compile(r"\bwindow\.open\s*\(\s*['\"]*javascript\s*:", re.I), "window.open con javascript:", 0.82),
|
|
97
104
|
]
|
|
98
105
|
|
|
99
106
|
# -------------------------------------------------
|
|
100
|
-
# Campos
|
|
107
|
+
# Campos sensibles: NO LOS IGNORAMOS COMPLETAMENTE, PERO LES DAMOS DESCUENTO EN SCORE
|
|
108
|
+
# Para robustez, los analizamos pero reducimos el peso para evitar falsos positivos.
|
|
101
109
|
# -------------------------------------------------
|
|
102
|
-
|
|
110
|
+
SENSITIVE_FIELDS = ["password", "csrfmiddlewaretoken", "token", "auth"]
|
|
111
|
+
SENSITIVE_DISCOUNT = 0.5 # Multiplicador para campos sensibles
|
|
103
112
|
|
|
104
113
|
# Umbral por defecto para considerar "alto riesgo" (Auditoria puede bloquear según su lógica)
|
|
105
114
|
XSS_DEFENSE_THRESHOLD = getattr(settings, "XSS_DEFENSE_THRESHOLD", 0.6)
|
|
106
115
|
|
|
107
|
-
|
|
108
116
|
# -------------------------------------------------
|
|
109
117
|
# Util: validación / extracción de IP (robusta)
|
|
110
118
|
# -------------------------------------------------
|
|
119
|
+
|
|
111
120
|
def _is_valid_ip(ip: str) -> bool:
|
|
112
121
|
"""Verifica que la cadena sea una IP válida (v4 o v6)."""
|
|
113
122
|
try:
|
|
@@ -117,7 +126,6 @@ def _is_valid_ip(ip: str) -> bool:
|
|
|
117
126
|
except Exception:
|
|
118
127
|
return False
|
|
119
128
|
|
|
120
|
-
|
|
121
129
|
def get_client_ip(request) -> str:
|
|
122
130
|
"""
|
|
123
131
|
Obtiene la mejor estimación de la IP del cliente:
|
|
@@ -143,7 +151,6 @@ def get_client_ip(request) -> str:
|
|
|
143
151
|
remote = request.META.get("REMOTE_ADDR")
|
|
144
152
|
return remote or ""
|
|
145
153
|
|
|
146
|
-
|
|
147
154
|
# -------------------------------------------------
|
|
148
155
|
# Extraer payload pero evitando cabeceras (para reducir falsos positivos)
|
|
149
156
|
# - Devuelve dict si es JSON, o dict con 'raw' para otros cuerpos
|
|
@@ -185,15 +192,14 @@ def extract_body_as_map(request) -> Dict[str, Any]:
|
|
|
185
192
|
pass
|
|
186
193
|
return {}
|
|
187
194
|
|
|
188
|
-
|
|
189
195
|
# -------------------------------------------------
|
|
190
196
|
# Analizar un solo valor (string) en busca de XSS usando patrones
|
|
191
197
|
# Devuelve (score, descripciones, matches_patterns)
|
|
192
198
|
# -------------------------------------------------
|
|
193
|
-
def detect_xss_in_value(value: str) -> Tuple[float, List[str], List[str]]:
|
|
199
|
+
def detect_xss_in_value(value: str, is_sensitive: bool = False) -> Tuple[float, List[str], List[str]]:
|
|
194
200
|
"""
|
|
195
201
|
Analiza una cadena y devuelve:
|
|
196
|
-
- score acumulado (sum pesos)
|
|
202
|
+
- score acumulado (sum pesos, con descuento si es campo sensible)
|
|
197
203
|
- lista de descripciones activadas
|
|
198
204
|
- lista de patrones (regex.pattern) que matchearon
|
|
199
205
|
"""
|
|
@@ -204,28 +210,26 @@ def detect_xss_in_value(value: str) -> Tuple[float, List[str], List[str]]:
|
|
|
204
210
|
descripcion = []
|
|
205
211
|
matches = []
|
|
206
212
|
|
|
207
|
-
# Si bleach está disponible,
|
|
213
|
+
# Si bleach está disponible, sanitizar y comparar para detección adicional
|
|
214
|
+
if _BLEACH_AVAILABLE:
|
|
215
|
+
cleaned = bleach.clean(value, strip=True)
|
|
216
|
+
if cleaned != value:
|
|
217
|
+
score_total += 0.5 # Penalización por cambios en sanitización
|
|
218
|
+
descripcion.append("Contenido alterado por sanitización (bleach)")
|
|
219
|
+
|
|
208
220
|
for patt, msg, weight in XSS_PATTERNS:
|
|
209
221
|
if patt.search(value):
|
|
210
|
-
|
|
222
|
+
adjusted_weight = weight * SENSITIVE_DISCOUNT if is_sensitive else weight
|
|
223
|
+
score_total += adjusted_weight
|
|
211
224
|
descripcion.append(msg)
|
|
212
225
|
matches.append(patt.pattern)
|
|
213
226
|
|
|
214
227
|
return round(score_total, 3), descripcion, matches
|
|
215
228
|
|
|
216
|
-
|
|
217
229
|
# -------------------------------------------------
|
|
218
|
-
# Middleware principal XSS
|
|
230
|
+
# Middleware principal XSS - MEJORADO PARA ROBUSTEZ
|
|
219
231
|
# -------------------------------------------------
|
|
220
232
|
class XSSDefenseMiddleware(MiddlewareMixin):
|
|
221
|
-
"""
|
|
222
|
-
Middleware para detección XSS.
|
|
223
|
-
- Analiza el body (campo por campo) y querystring si aplica.
|
|
224
|
-
- Ignora campos sensibles (password, token).
|
|
225
|
-
- No incluye User-Agent/Referer en el texto analizado (evita falsos positivos).
|
|
226
|
-
- Añade request.xss_attack_info con: ip, tipos, descripcion, payload, score, url.
|
|
227
|
-
"""
|
|
228
|
-
|
|
229
233
|
def process_request(self, request):
|
|
230
234
|
# 1) IP y exclusiones
|
|
231
235
|
client_ip = get_client_ip(request)
|
|
@@ -251,15 +255,12 @@ class XSSDefenseMiddleware(MiddlewareMixin):
|
|
|
251
255
|
total_score = 0.0
|
|
252
256
|
all_descriptions: List[str] = []
|
|
253
257
|
all_matches: List[str] = []
|
|
254
|
-
# payload_for_storage: guardamos un resumen/truncado para auditoría
|
|
255
258
|
payload_summary = []
|
|
256
259
|
|
|
257
|
-
# 3) Analizar campo por campo (si es dict) o el raw
|
|
260
|
+
# 3) Analizar campo por campo (si es dict) o el raw - AHORA ANALIZA TODO, CON DESCUENTO PARA SENSIBLES
|
|
258
261
|
if isinstance(data, dict):
|
|
259
262
|
for key, value in data.items():
|
|
260
|
-
|
|
261
|
-
if isinstance(key, str) and key.lower() in [f.lower() for f in IGNORED_FIELDS]:
|
|
262
|
-
continue
|
|
263
|
+
is_sensitive = isinstance(key, str) and key.lower() in SENSITIVE_FIELDS
|
|
263
264
|
|
|
264
265
|
# convertir a string si es otro tipo (list, int...)
|
|
265
266
|
if isinstance(value, (dict, list)):
|
|
@@ -270,20 +271,13 @@ class XSSDefenseMiddleware(MiddlewareMixin):
|
|
|
270
271
|
else:
|
|
271
272
|
vtext = str(value or "")
|
|
272
273
|
|
|
273
|
-
|
|
274
|
-
# las probabilidades de XSS son muy bajas; continúa (reduce falsos positivos).
|
|
275
|
-
if key.lower() in ("email", "username") and len(vtext) < 256:
|
|
276
|
-
# aún así pasar por patrones (no lo ignoramos completamente), pero podemos bajar sensibilidad
|
|
277
|
-
pass
|
|
278
|
-
|
|
279
|
-
s, descs, matches = detect_xss_in_value(vtext)
|
|
274
|
+
s, descs, matches = detect_xss_in_value(vtext, is_sensitive)
|
|
280
275
|
total_score += s
|
|
281
276
|
all_descriptions.extend(descs)
|
|
282
277
|
all_matches.extend(matches)
|
|
283
278
|
|
|
284
279
|
if s > 0:
|
|
285
|
-
|
|
286
|
-
payload_summary.append({ "field": key, "snippet": vtext[:300] })
|
|
280
|
+
payload_summary.append({"field": key, "snippet": vtext[:300], "sensitive": is_sensitive})
|
|
287
281
|
|
|
288
282
|
else:
|
|
289
283
|
# si no es dict, analizar el raw como texto
|
|
@@ -293,7 +287,7 @@ class XSSDefenseMiddleware(MiddlewareMixin):
|
|
|
293
287
|
all_descriptions.extend(descs)
|
|
294
288
|
all_matches.extend(matches)
|
|
295
289
|
if s > 0:
|
|
296
|
-
payload_summary.append({"field":"raw","snippet": raw[:500]})
|
|
290
|
+
payload_summary.append({"field": "raw", "snippet": raw[:500], "sensitive": False})
|
|
297
291
|
|
|
298
292
|
# 4) si no detectó nada, continuar
|
|
299
293
|
if total_score == 0:
|
|
@@ -305,7 +299,7 @@ class XSSDefenseMiddleware(MiddlewareMixin):
|
|
|
305
299
|
payload_for_request = json.dumps(payload_summary, ensure_ascii=False)[:2000]
|
|
306
300
|
|
|
307
301
|
logger.warning(
|
|
308
|
-
"XSS detectado desde IP %s URL=%s Score=%.3f Desc=%s",
|
|
302
|
+
"XSS detectado desde IP %s URL=%s Score=%.3f Desc=%s (Robust: incluye sensibles con descuento)",
|
|
309
303
|
client_ip,
|
|
310
304
|
url,
|
|
311
305
|
score_rounded,
|
|
@@ -320,8 +314,7 @@ class XSSDefenseMiddleware(MiddlewareMixin):
|
|
|
320
314
|
"payload": payload_for_request,
|
|
321
315
|
"score": score_rounded,
|
|
322
316
|
"url": url,
|
|
323
|
-
}
|
|
324
|
-
|
|
317
|
+
}
|
|
325
318
|
# 7) NO bloquear aquí — lo hace AuditoriaMiddleware según su política
|
|
326
319
|
return None
|
|
327
320
|
|