GuardianUnivalle-Benito-Yucra 0.1.68__py3-none-any.whl → 0.1.70__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 +111 -20
- GuardianUnivalle_Benito_Yucra/detectores/detector_sql.py +94 -36
- GuardianUnivalle_Benito_Yucra/detectores/detector_xss.py +37 -35
- {guardianunivalle_benito_yucra-0.1.68.dist-info → guardianunivalle_benito_yucra-0.1.70.dist-info}/METADATA +3 -5
- {guardianunivalle_benito_yucra-0.1.68.dist-info → guardianunivalle_benito_yucra-0.1.70.dist-info}/RECORD +8 -8
- {guardianunivalle_benito_yucra-0.1.68.dist-info → guardianunivalle_benito_yucra-0.1.70.dist-info}/WHEEL +0 -0
- {guardianunivalle_benito_yucra-0.1.68.dist-info → guardianunivalle_benito_yucra-0.1.70.dist-info}/licenses/LICENSE +0 -0
- {guardianunivalle_benito_yucra-0.1.68.dist-info → guardianunivalle_benito_yucra-0.1.70.dist-info}/top_level.txt +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,40 @@ 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
|
-
|
|
143
|
+
def extract_payload_as_map(request) -> Dict[str, Any]:
|
|
144
|
+
"""
|
|
145
|
+
Extrae un diccionario con los datos a analizar:
|
|
146
|
+
- Si JSON: devuelve el dict JSON.
|
|
147
|
+
- Si form-data: devuelve request.POST.dict()
|
|
148
|
+
- Si otro: devuelve {'raw': <texto>}
|
|
149
|
+
"""
|
|
131
150
|
try:
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
151
|
+
ct = request.META.get("CONTENT_TYPE", "")
|
|
152
|
+
if "application/json" in ct:
|
|
153
|
+
raw = request.body.decode("utf-8") or "{}"
|
|
154
|
+
try:
|
|
155
|
+
data = json.loads(raw)
|
|
156
|
+
if isinstance(data, dict):
|
|
157
|
+
return data
|
|
158
|
+
else:
|
|
159
|
+
return {"raw": raw}
|
|
160
|
+
except Exception:
|
|
161
|
+
return {"raw": raw}
|
|
135
162
|
else:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
163
|
+
try:
|
|
164
|
+
post = request.POST.dict()
|
|
165
|
+
if post:
|
|
166
|
+
return post
|
|
167
|
+
except Exception:
|
|
168
|
+
pass
|
|
169
|
+
raw = request.body.decode("utf-8", errors="ignore")
|
|
170
|
+
if raw:
|
|
171
|
+
return {"raw": raw}
|
|
139
172
|
except Exception:
|
|
140
173
|
pass
|
|
141
|
-
|
|
142
|
-
qs = request.META.get("QUERY_STRING", "")
|
|
143
|
-
if qs:
|
|
144
|
-
parts.append(qs)
|
|
145
|
-
|
|
146
|
-
return " ".join(parts)
|
|
174
|
+
return {}
|
|
147
175
|
|
|
148
176
|
# ----------------------------
|
|
149
177
|
# Normalización / preprocesamiento
|
|
@@ -159,13 +187,12 @@ def normalize_input(s: str) -> str:
|
|
|
159
187
|
s_dec = html.unescape(s_dec)
|
|
160
188
|
except Exception:
|
|
161
189
|
pass
|
|
162
|
-
# Reemplazo seguro de secuencias \xNN
|
|
163
190
|
s_dec = re.sub(r"\\x([0-9a-fA-F]{2})", r"\\x\g<1>", s_dec)
|
|
164
191
|
s_dec = re.sub(r"\s+", " ", s_dec)
|
|
165
192
|
return s_dec.strip()
|
|
166
193
|
|
|
167
194
|
# ----------------------------
|
|
168
|
-
# Detector SQLi
|
|
195
|
+
# Detector SQLi - ROBUSTO SIN DESCUENTO
|
|
169
196
|
# ----------------------------
|
|
170
197
|
def detect_sql_injection(text: str) -> Dict:
|
|
171
198
|
norm = normalize_input(text or "")
|
|
@@ -174,7 +201,7 @@ def detect_sql_injection(text: str) -> Dict:
|
|
|
174
201
|
descriptions = []
|
|
175
202
|
for pattern, desc, weight in SQL_PATTERNS:
|
|
176
203
|
if pattern.search(norm):
|
|
177
|
-
score += weight
|
|
204
|
+
score += weight # Score full, sin descuento
|
|
178
205
|
matches.append((desc, pattern.pattern, weight))
|
|
179
206
|
descriptions.append(desc)
|
|
180
207
|
|
|
@@ -185,14 +212,8 @@ def detect_sql_injection(text: str) -> Dict:
|
|
|
185
212
|
"sample": norm[:1200],
|
|
186
213
|
}
|
|
187
214
|
|
|
188
|
-
DEFAULT_THRESHOLDS = {
|
|
189
|
-
"HIGH": 1.8,
|
|
190
|
-
"MEDIUM": 1.0,
|
|
191
|
-
"LOW": 0.5,
|
|
192
|
-
}
|
|
193
|
-
|
|
194
215
|
# ----------------------------
|
|
195
|
-
# Middleware SQLi
|
|
216
|
+
# Middleware SQLi - ULTRA-ROBUSTO
|
|
196
217
|
# ----------------------------
|
|
197
218
|
class SQLIDefenseMiddleware(MiddlewareMixin):
|
|
198
219
|
def process_request(self, request):
|
|
@@ -208,27 +229,64 @@ class SQLIDefenseMiddleware(MiddlewareMixin):
|
|
|
208
229
|
if any(url in referer for url in trusted_urls) or any(url in host for url in trusted_urls):
|
|
209
230
|
return None
|
|
210
231
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
232
|
+
# Extraer datos como mapa para análisis por campo
|
|
233
|
+
data = extract_payload_as_map(request)
|
|
234
|
+
qs = request.META.get("QUERY_STRING", "")
|
|
235
|
+
if qs:
|
|
236
|
+
data["_query_string"] = qs
|
|
237
|
+
|
|
238
|
+
if not data:
|
|
239
|
+
return None
|
|
215
240
|
|
|
216
|
-
|
|
241
|
+
total_score = 0.0
|
|
242
|
+
all_descriptions = []
|
|
243
|
+
all_matches = []
|
|
244
|
+
payload_summary = []
|
|
245
|
+
|
|
246
|
+
# Analizar campo por campo - AHORA SIN DESCUENTO PARA ROBUSTEZ
|
|
247
|
+
if isinstance(data, dict):
|
|
248
|
+
for key, value in data.items():
|
|
249
|
+
if isinstance(value, (dict, list)):
|
|
250
|
+
try:
|
|
251
|
+
vtext = json.dumps(value, ensure_ascii=False)
|
|
252
|
+
except Exception:
|
|
253
|
+
vtext = str(value)
|
|
254
|
+
else:
|
|
255
|
+
vtext = str(value or "")
|
|
256
|
+
|
|
257
|
+
result = detect_sql_injection(vtext)
|
|
258
|
+
total_score += result["score"]
|
|
259
|
+
all_descriptions.extend(result["descriptions"])
|
|
260
|
+
all_matches.extend(result["matches"])
|
|
261
|
+
|
|
262
|
+
if result["score"] > 0:
|
|
263
|
+
is_sensitive = isinstance(key, str) and key.lower() in SENSITIVE_FIELDS
|
|
264
|
+
payload_summary.append({"field": key, "snippet": vtext[:300], "sensitive": is_sensitive})
|
|
265
|
+
else:
|
|
266
|
+
raw = str(data)
|
|
267
|
+
result = detect_sql_injection(raw)
|
|
268
|
+
total_score += result["score"]
|
|
269
|
+
all_descriptions.extend(result["descriptions"])
|
|
270
|
+
all_matches.extend(result["matches"])
|
|
271
|
+
if result["score"] > 0:
|
|
272
|
+
payload_summary.append({"field": "raw", "snippet": raw[:500], "sensitive": False})
|
|
273
|
+
|
|
274
|
+
if total_score == 0:
|
|
217
275
|
return None
|
|
218
276
|
|
|
219
277
|
# Registrar ataque
|
|
220
278
|
logger.warning(
|
|
221
279
|
f"[SQLiDetect] IP={client_ip} Host={host} Referer={referer} "
|
|
222
|
-
f"Score={
|
|
280
|
+
f"Score={total_score:.2f} Desc={all_descriptions} Payload={json.dumps(payload_summary, ensure_ascii=False)[:500]}"
|
|
223
281
|
)
|
|
224
282
|
|
|
225
283
|
# Guardar info en request
|
|
226
284
|
request.sql_attack_info = {
|
|
227
285
|
"ip": client_ip,
|
|
228
286
|
"tipos": ["SQLi"],
|
|
229
|
-
"descripcion":
|
|
230
|
-
"payload":
|
|
231
|
-
"score": round(
|
|
287
|
+
"descripcion": all_descriptions,
|
|
288
|
+
"payload": json.dumps(payload_summary, ensure_ascii=False)[:1000],
|
|
289
|
+
"score": round(total_score, 2),
|
|
232
290
|
"url": request.build_absolute_uri(),
|
|
233
291
|
}
|
|
234
292
|
|
|
@@ -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,17 +93,26 @@ 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
|
# -------------------------------------------------
|
|
@@ -117,7 +125,6 @@ def _is_valid_ip(ip: str) -> bool:
|
|
|
117
125
|
except Exception:
|
|
118
126
|
return False
|
|
119
127
|
|
|
120
|
-
|
|
121
128
|
def get_client_ip(request) -> str:
|
|
122
129
|
"""
|
|
123
130
|
Obtiene la mejor estimación de la IP del cliente:
|
|
@@ -143,7 +150,6 @@ def get_client_ip(request) -> str:
|
|
|
143
150
|
remote = request.META.get("REMOTE_ADDR")
|
|
144
151
|
return remote or ""
|
|
145
152
|
|
|
146
|
-
|
|
147
153
|
# -------------------------------------------------
|
|
148
154
|
# Extraer payload pero evitando cabeceras (para reducir falsos positivos)
|
|
149
155
|
# - Devuelve dict si es JSON, o dict con 'raw' para otros cuerpos
|
|
@@ -185,15 +191,14 @@ def extract_body_as_map(request) -> Dict[str, Any]:
|
|
|
185
191
|
pass
|
|
186
192
|
return {}
|
|
187
193
|
|
|
188
|
-
|
|
189
194
|
# -------------------------------------------------
|
|
190
195
|
# Analizar un solo valor (string) en busca de XSS usando patrones
|
|
191
196
|
# Devuelve (score, descripciones, matches_patterns)
|
|
192
197
|
# -------------------------------------------------
|
|
193
|
-
def detect_xss_in_value(value: str) -> Tuple[float, List[str], List[str]]:
|
|
198
|
+
def detect_xss_in_value(value: str, is_sensitive: bool = False) -> Tuple[float, List[str], List[str]]:
|
|
194
199
|
"""
|
|
195
200
|
Analiza una cadena y devuelve:
|
|
196
|
-
- score acumulado (sum pesos)
|
|
201
|
+
- score acumulado (sum pesos, con descuento si es campo sensible)
|
|
197
202
|
- lista de descripciones activadas
|
|
198
203
|
- lista de patrones (regex.pattern) que matchearon
|
|
199
204
|
"""
|
|
@@ -204,25 +209,31 @@ def detect_xss_in_value(value: str) -> Tuple[float, List[str], List[str]]:
|
|
|
204
209
|
descripcion = []
|
|
205
210
|
matches = []
|
|
206
211
|
|
|
207
|
-
# Si bleach está disponible,
|
|
212
|
+
# Si bleach está disponible, sanitizar y comparar para detección adicional
|
|
213
|
+
if _BLEACH_AVAILABLE:
|
|
214
|
+
cleaned = bleach.clean(value, strip=True)
|
|
215
|
+
if cleaned != value:
|
|
216
|
+
score_total += 0.5 # Penalización por cambios en sanitización
|
|
217
|
+
descripcion.append("Contenido alterado por sanitización (bleach)")
|
|
218
|
+
|
|
208
219
|
for patt, msg, weight in XSS_PATTERNS:
|
|
209
220
|
if patt.search(value):
|
|
210
|
-
|
|
221
|
+
adjusted_weight = weight * SENSITIVE_DISCOUNT if is_sensitive else weight
|
|
222
|
+
score_total += adjusted_weight
|
|
211
223
|
descripcion.append(msg)
|
|
212
224
|
matches.append(patt.pattern)
|
|
213
225
|
|
|
214
226
|
return round(score_total, 3), descripcion, matches
|
|
215
227
|
|
|
216
|
-
|
|
217
228
|
# -------------------------------------------------
|
|
218
|
-
# Middleware principal XSS
|
|
229
|
+
# Middleware principal XSS - MEJORADO PARA ROBUSTEZ
|
|
219
230
|
# -------------------------------------------------
|
|
220
231
|
class XSSDefenseMiddleware(MiddlewareMixin):
|
|
221
232
|
"""
|
|
222
|
-
Middleware para detección XSS.
|
|
223
|
-
- Analiza el body (campo por campo) y querystring
|
|
224
|
-
-
|
|
225
|
-
-
|
|
233
|
+
Middleware para detección XSS - Versión Robusta.
|
|
234
|
+
- Analiza TODO el body (campo por campo) y querystring.
|
|
235
|
+
- Campos sensibles se analizan con descuento en score para evitar falsos positivos.
|
|
236
|
+
- Usa bleach si disponible para sanitización.
|
|
226
237
|
- Añade request.xss_attack_info con: ip, tipos, descripcion, payload, score, url.
|
|
227
238
|
"""
|
|
228
239
|
|
|
@@ -251,15 +262,12 @@ class XSSDefenseMiddleware(MiddlewareMixin):
|
|
|
251
262
|
total_score = 0.0
|
|
252
263
|
all_descriptions: List[str] = []
|
|
253
264
|
all_matches: List[str] = []
|
|
254
|
-
# payload_for_storage: guardamos un resumen/truncado para auditoría
|
|
255
265
|
payload_summary = []
|
|
256
266
|
|
|
257
|
-
# 3) Analizar campo por campo (si es dict) o el raw
|
|
267
|
+
# 3) Analizar campo por campo (si es dict) o el raw - AHORA ANALIZA TODO, CON DESCUENTO PARA SENSIBLES
|
|
258
268
|
if isinstance(data, dict):
|
|
259
269
|
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
|
|
270
|
+
is_sensitive = isinstance(key, str) and key.lower() in SENSITIVE_FIELDS
|
|
263
271
|
|
|
264
272
|
# convertir a string si es otro tipo (list, int...)
|
|
265
273
|
if isinstance(value, (dict, list)):
|
|
@@ -270,20 +278,13 @@ class XSSDefenseMiddleware(MiddlewareMixin):
|
|
|
270
278
|
else:
|
|
271
279
|
vtext = str(value or "")
|
|
272
280
|
|
|
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)
|
|
281
|
+
s, descs, matches = detect_xss_in_value(vtext, is_sensitive)
|
|
280
282
|
total_score += s
|
|
281
283
|
all_descriptions.extend(descs)
|
|
282
284
|
all_matches.extend(matches)
|
|
283
285
|
|
|
284
286
|
if s > 0:
|
|
285
|
-
|
|
286
|
-
payload_summary.append({ "field": key, "snippet": vtext[:300] })
|
|
287
|
+
payload_summary.append({"field": key, "snippet": vtext[:300], "sensitive": is_sensitive})
|
|
287
288
|
|
|
288
289
|
else:
|
|
289
290
|
# si no es dict, analizar el raw como texto
|
|
@@ -293,7 +294,7 @@ class XSSDefenseMiddleware(MiddlewareMixin):
|
|
|
293
294
|
all_descriptions.extend(descs)
|
|
294
295
|
all_matches.extend(matches)
|
|
295
296
|
if s > 0:
|
|
296
|
-
payload_summary.append({"field":"raw","snippet": raw[:500]})
|
|
297
|
+
payload_summary.append({"field": "raw", "snippet": raw[:500], "sensitive": False})
|
|
297
298
|
|
|
298
299
|
# 4) si no detectó nada, continuar
|
|
299
300
|
if total_score == 0:
|
|
@@ -305,7 +306,7 @@ class XSSDefenseMiddleware(MiddlewareMixin):
|
|
|
305
306
|
payload_for_request = json.dumps(payload_summary, ensure_ascii=False)[:2000]
|
|
306
307
|
|
|
307
308
|
logger.warning(
|
|
308
|
-
"XSS detectado desde IP %s URL=%s Score=%.3f Desc=%s",
|
|
309
|
+
"XSS detectado desde IP %s URL=%s Score=%.3f Desc=%s (Robust: incluye sensibles con descuento)",
|
|
309
310
|
client_ip,
|
|
310
311
|
url,
|
|
311
312
|
score_rounded,
|
|
@@ -322,6 +323,7 @@ class XSSDefenseMiddleware(MiddlewareMixin):
|
|
|
322
323
|
"url": url,
|
|
323
324
|
}
|
|
324
325
|
|
|
326
|
+
|
|
325
327
|
# 7) NO bloquear aquí — lo hace AuditoriaMiddleware según su política
|
|
326
328
|
return None
|
|
327
329
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: GuardianUnivalle-Benito-Yucra
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.70
|
|
4
4
|
Summary: Middleware y detectores de seguridad (SQLi, XSS, CSRF, DoS) para Django/Flask
|
|
5
5
|
Author-email: Andres Benito Calle Yucra <benitoandrescalle035@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -23,11 +23,9 @@ Dynamic: license-file
|
|
|
23
23
|
|
|
24
24
|
<!-- Información de la librería -->
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
# 🛡️ Guardian Univalle – Benito & Junkrat
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
Framework de detección y defensa de amenazas web para Django
|
|
28
|
+
Framework de detección y defensa de amenazas web para Django y Flask
|
|
31
29
|
|
|
32
30
|
Guardian Univalle es un sistema de seguridad modular desarrollado para fortalecer aplicaciones Django frente a ataques web comunes como XSS, CSRF, inyección SQL, ataques DoS y scraping automatizado.
|
|
33
31
|
Cada módulo opera mediante middleware independientes que analizan el tráfico HTTP en tiempo real, aplican heurísticas inteligentes y registran eventos sospechosos para auditoría y bloqueo adaptativo.
|
|
@@ -4,16 +4,16 @@ 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=cVc5-rIwgOuFkov1tHtUR2PnyFdF3Ns6RvJMq4hkuOU,12486
|
|
8
8
|
GuardianUnivalle_Benito_Yucra/detectores/detector_dos.py,sha256=Jy4fhI-6n9wQR0quzpondcUyCA2447lDq4fmOFeM1jA,14989
|
|
9
|
-
GuardianUnivalle_Benito_Yucra/detectores/detector_sql.py,sha256=
|
|
10
|
-
GuardianUnivalle_Benito_Yucra/detectores/detector_xss.py,sha256=
|
|
9
|
+
GuardianUnivalle_Benito_Yucra/detectores/detector_sql.py,sha256=VS_N9hfEsAYQqgGSKbVyhO2AbNHVerf5xCOzV6_IuG0,14977
|
|
10
|
+
GuardianUnivalle_Benito_Yucra/detectores/detector_xss.py,sha256=WyMrehYDlVYu9jNFal3QcKoRJRe0Z9Jle6qHNxDDRjg,15331
|
|
11
11
|
GuardianUnivalle_Benito_Yucra/middleware_web/middleware_web.py,sha256=23pLLYqliUoMrIC6ZEwz3hKXeDjWfHSm9vYPWGmDDik,495
|
|
12
12
|
GuardianUnivalle_Benito_Yucra/mitigacion/limitador_peticion.py,sha256=ipMOebYhql-6mSyHs0ddYXOcXq9w8P_IXLlpiIqGncw,246
|
|
13
13
|
GuardianUnivalle_Benito_Yucra/mitigacion/lista_bloqueo.py,sha256=6AYWII4mrmwCLHCvGTyoBxR4Oasr4raSHpFbVjqn7d8,193
|
|
14
14
|
GuardianUnivalle_Benito_Yucra/puntuacion/puntuacion_amenaza.py,sha256=Wx5XfcII4oweLvZsTBEJ7kUc9pMpP5-36RfI5C5KJXo,561
|
|
15
|
-
guardianunivalle_benito_yucra-0.1.
|
|
16
|
-
guardianunivalle_benito_yucra-0.1.
|
|
17
|
-
guardianunivalle_benito_yucra-0.1.
|
|
18
|
-
guardianunivalle_benito_yucra-0.1.
|
|
19
|
-
guardianunivalle_benito_yucra-0.1.
|
|
15
|
+
guardianunivalle_benito_yucra-0.1.70.dist-info/licenses/LICENSE,sha256=5e4IdL542v1E8Ft0A24GZjrxZeTsVK7XrS3mZEUhPtM,37
|
|
16
|
+
guardianunivalle_benito_yucra-0.1.70.dist-info/METADATA,sha256=ix2XVuVbdm96qMcox9hByCAh0EPjnKUbFrlR_rTPMSE,7534
|
|
17
|
+
guardianunivalle_benito_yucra-0.1.70.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
18
|
+
guardianunivalle_benito_yucra-0.1.70.dist-info/top_level.txt,sha256=HTWfZM64WAV_QYr5cnXnLuabQt92dvlxqlR3pCwpbDQ,30
|
|
19
|
+
guardianunivalle_benito_yucra-0.1.70.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|