GuardianUnivalle-Benito-Yucra 1.1.18__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.
- GuardianUnivalle_Benito_Yucra/__init__.py +25 -0
- GuardianUnivalle_Benito_Yucra/auditoria/registro_auditoria.py +40 -0
- GuardianUnivalle_Benito_Yucra/criptografia/cifrado_aead.py +25 -0
- GuardianUnivalle_Benito_Yucra/criptografia/intercambio_claves.py +23 -0
- GuardianUnivalle_Benito_Yucra/criptografia/kdf.py +23 -0
- GuardianUnivalle_Benito_Yucra/detectores/detector_csrf.py +488 -0
- GuardianUnivalle_Benito_Yucra/detectores/detector_dos.py +352 -0
- GuardianUnivalle_Benito_Yucra/detectores/detector_sql.py +641 -0
- GuardianUnivalle_Benito_Yucra/detectores/detector_xss.py +626 -0
- GuardianUnivalle_Benito_Yucra/middleware_web/middleware_web.py +13 -0
- GuardianUnivalle_Benito_Yucra/mitigacion/limitador_peticion.py +7 -0
- GuardianUnivalle_Benito_Yucra/mitigacion/lista_bloqueo.py +10 -0
- GuardianUnivalle_Benito_Yucra/puntuacion/puntuacion_amenaza.py +15 -0
- GuardianUnivalle_Benito_Yucra/utilidades.py +7 -0
- guardianunivalle_benito_yucra-1.1.18.dist-info/METADATA +279 -0
- guardianunivalle_benito_yucra-1.1.18.dist-info/RECORD +19 -0
- guardianunivalle_benito_yucra-1.1.18.dist-info/WHEEL +5 -0
- guardianunivalle_benito_yucra-1.1.18.dist-info/licenses/LICENSE +1 -0
- guardianunivalle_benito_yucra-1.1.18.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
# xss_defense_crypto.py
|
|
2
|
+
# GuardianUnivalle_Benito_Yucra/detectores/xss_defense_crypto.py
|
|
3
|
+
# Middleware robusto para detección y mitigación de XSS con componentes criptográficos integrados
|
|
4
|
+
# - Detección por patrones con pesos y saturación
|
|
5
|
+
# - Sanitización con bleach (si disponible)
|
|
6
|
+
# - Integración de HMAC-SHA256 para firmar tokens/cookies
|
|
7
|
+
# - SHA-256/SHA-3 para hashes de contenido
|
|
8
|
+
# - AES-GCM/ChaCha20-Poly1305 para cifrar cookies sensibles
|
|
9
|
+
# - HKDF para derivar claves
|
|
10
|
+
# - Argon2id para seguridad de claves derivadas
|
|
11
|
+
# - TLS 1.3 recomendado en configuración del servidor (no implementado aquí, pero documentado)
|
|
12
|
+
# - Registro cifrado de eventos y payloads
|
|
13
|
+
# xss_defense_crypto.py
|
|
14
|
+
# GuardianUnivalle_Benito_Yucra/detectores/xss_defense_crypto.py
|
|
15
|
+
# Middleware robusto para detección y mitigación de XSS con componentes criptográficos integrados
|
|
16
|
+
# - Detección por patrones con pesos y saturación
|
|
17
|
+
# - Sanitización con bleach (si disponible)
|
|
18
|
+
# - Integración de HMAC-SHA256 para firmar tokens/cookies
|
|
19
|
+
# - SHA-256/SHA-3 para hashes de contenido
|
|
20
|
+
# - AES-GCM/ChaCha20-Poly1305 para cifrar cookies sensibles
|
|
21
|
+
# - HKDF para derivar claves
|
|
22
|
+
# - Argon2id para seguridad de claves derivadas
|
|
23
|
+
# - TLS 1.3 recomendado en configuración del servidor (no implementado aquí, pero documentado)
|
|
24
|
+
# - Registro cifrado de eventos y payloads
|
|
25
|
+
# xss_defense_crypto.py
|
|
26
|
+
# GuardianUnivalle_Benito_Yucra/detectores/xss_defense_crypto.py
|
|
27
|
+
# Middleware robusto para detección y mitigación de XSS con componentes criptográficos integrados
|
|
28
|
+
# - Detección por patrones con pesos y saturación
|
|
29
|
+
# - Sanitización con bleach (si disponible)
|
|
30
|
+
# - Integración de HMAC-SHA256 para firmar tokens/cookies
|
|
31
|
+
# - SHA-256/SHA-3 para hashes de contenido
|
|
32
|
+
# - AES-GCM/ChaCha20-Poly1305 para cifrar cookies sensibles
|
|
33
|
+
# - HKDF para derivar claves
|
|
34
|
+
# - Argon2id para seguridad de claves derivadas
|
|
35
|
+
# - TLS 1.3 recomendado en configuración del servidor (no implementado aquí, pero documentado)
|
|
36
|
+
# - Registro cifrado de eventos y payloads
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
import json
|
|
40
|
+
import logging
|
|
41
|
+
import re
|
|
42
|
+
import math
|
|
43
|
+
import base64
|
|
44
|
+
import os
|
|
45
|
+
import time
|
|
46
|
+
from typing import List, Tuple, Dict, Any
|
|
47
|
+
from django.utils.deprecation import MiddlewareMixin
|
|
48
|
+
from django.conf import settings
|
|
49
|
+
from django.http import HttpResponseForbidden, HttpResponse
|
|
50
|
+
from django.core.cache import cache
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
import bleach
|
|
54
|
+
_BLEACH_AVAILABLE = True
|
|
55
|
+
except Exception:
|
|
56
|
+
_BLEACH_AVAILABLE = False
|
|
57
|
+
|
|
58
|
+
# cryptography & argon2
|
|
59
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM, ChaCha20Poly1305
|
|
60
|
+
from cryptography.hazmat.primitives import hashes, hmac as crypto_hmac
|
|
61
|
+
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
|
62
|
+
from cryptography.exceptions import InvalidSignature
|
|
63
|
+
from argon2.low_level import hash_secret_raw, Type as Argon2Type
|
|
64
|
+
|
|
65
|
+
# ----------------------------
|
|
66
|
+
# Logger
|
|
67
|
+
# ----------------------------
|
|
68
|
+
logger = logging.getLogger("xssdefense_crypto")
|
|
69
|
+
logger.setLevel(logging.INFO)
|
|
70
|
+
if not logger.handlers:
|
|
71
|
+
handler = logging.StreamHandler()
|
|
72
|
+
handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
|
|
73
|
+
logger.addHandler(handler)
|
|
74
|
+
|
|
75
|
+
# ----------------------------
|
|
76
|
+
# Configuraciones criptográficas (similar a SQLi)
|
|
77
|
+
# ----------------------------
|
|
78
|
+
MASTER_KEY_B64 = getattr(settings, "XSS_DEFENSE_MASTER_KEY", None)
|
|
79
|
+
if not MASTER_KEY_B64:
|
|
80
|
+
MASTER_KEY = os.urandom(32)
|
|
81
|
+
else:
|
|
82
|
+
try:
|
|
83
|
+
MASTER_KEY = base64.b64decode(MASTER_KEY_B64)
|
|
84
|
+
except Exception:
|
|
85
|
+
MASTER_KEY = MASTER_KEY_B64.encode() if isinstance(MASTER_KEY_B64, str) else MASTER_KEY_B64
|
|
86
|
+
|
|
87
|
+
AEAD_CHOICE = getattr(settings, "XSS_DEFENSE_AEAD", "AESGCM").upper() # AESGCM o CHACHA20
|
|
88
|
+
ARGON2_CONFIG = getattr(settings, "XSS_DEFENSE_ARGON2", {
|
|
89
|
+
"time_cost": 2,
|
|
90
|
+
"memory_cost": 65536,
|
|
91
|
+
"parallelism": 1,
|
|
92
|
+
"hash_len": 32,
|
|
93
|
+
"type": Argon2Type.ID,
|
|
94
|
+
})
|
|
95
|
+
HMAC_LABEL = b"xssdefense-hmac"
|
|
96
|
+
AEAD_LABEL = b"xssdefense-aead"
|
|
97
|
+
HASH_CHOICE = getattr(settings, "XSS_DEFENSE_HASH", "SHA256").upper() # SHA256 o SHA3
|
|
98
|
+
|
|
99
|
+
# ----------------------------
|
|
100
|
+
# Configuraciones de bloqueo y cache (similar a SQLi, pero con prefijo XSS_)
|
|
101
|
+
# ----------------------------
|
|
102
|
+
XSS_BLOCK_TIMEOUT = getattr(settings, "XSS_DEFENSE_BLOCK_SECONDS", 60 * 60)
|
|
103
|
+
XSS_COUNTER_WINDOW = getattr(settings, "XSS_DEFENSE_COUNTER_WINDOW", 60 * 5)
|
|
104
|
+
XSS_COUNTER_THRESHOLD = getattr(settings, "XSS_DEFENSE_COUNTER_THRESHOLD", 5)
|
|
105
|
+
XSS_CACHE_BLOCK_KEY_PREFIX = "xss_block:"
|
|
106
|
+
XSS_CACHE_COUNTER_KEY_PREFIX = "xss_count:"
|
|
107
|
+
XSS_DEFAULT_BACKOFF_LEVELS = getattr(settings, "XSS_DEFENSE_BACKOFF_LEVELS", [0, 60 * 15, 60 * 60, 60 * 60 * 6, 60 * 60 * 24, 60 * 60 * 24 * 7])
|
|
108
|
+
XSS_NORM_THRESHOLDS = {
|
|
109
|
+
"HIGH": getattr(settings, "XSS_DEFENSE_NORM_HIGH", 0.2),
|
|
110
|
+
"MEDIUM": getattr(settings, "XSS_DEFENSE_NORM_MED", 0.1),
|
|
111
|
+
"LOW": getattr(settings, "XSS_DEFENSE_NORM_LOW", 0.05),
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# ----------------------------
|
|
115
|
+
# Patrones XSS robustos (igual que antes)
|
|
116
|
+
# ----------------------------
|
|
117
|
+
XSS_PATTERNS: List[Tuple[re.Pattern, str, float]] = [
|
|
118
|
+
(re.compile(r"<\s*script\b", re.I), "<script> directo", 0.95),
|
|
119
|
+
(re.compile(r"<\s*s\s*c\s*r\s*i\s*p\s*t\b", re.I), "<script> ofuscado", 0.90),
|
|
120
|
+
(re.compile(r"\b(eval|Function|setTimeout|setInterval|document\.write)\s*\(", re.I),
|
|
121
|
+
"Ejecución JS dinámica", 0.88),
|
|
122
|
+
(re.compile(r"\bjavascript\s*:", re.I), "URI javascript:", 0.85),
|
|
123
|
+
(re.compile(r"\bdata\s*:\s*text\/html\b", re.I), "URI data:text/html", 0.82),
|
|
124
|
+
(re.compile(r"\bvbscript\s*:", re.I), "URI vbscript:", 0.7),
|
|
125
|
+
(re.compile(r"<\s*(iframe|embed|object|svg|math|meta)\b", re.I), "Iframe/Embed/Object/SVG/Meta", 0.88),
|
|
126
|
+
(re.compile(r"<\s*img\b[^>]*\bonerror\b", re.I), "<img onerror>", 0.86),
|
|
127
|
+
(re.compile(r"<\s*svg\b[^>]*\bonload\b", re.I), "SVG onload/on*", 0.84),
|
|
128
|
+
(re.compile(r"\s+on[a-zA-Z]+\s*=", re.I), "Atributo evento on*", 0.80),
|
|
129
|
+
(re.compile(r"<\s*(a|img|body|div|span|form|input|button)\b[^>]*on[a-zA-Z]+\s*=", re.I),
|
|
130
|
+
"Elemento con evento on*", 0.82),
|
|
131
|
+
(re.compile(r"\binnerHTML\s*=\s*.*[<>\"']", re.I), "Asignación innerHTML", 0.85),
|
|
132
|
+
(re.compile(r"\bdocument\.getElementById\s*\(\s*.*\)\.innerHTML", re.I), "Manipulación DOM innerHTML", 0.80),
|
|
133
|
+
(re.compile(r"\bJSON\.parse\(|\beval\(\s*JSON", re.I), "JSON parse/eval inseguro", 0.75),
|
|
134
|
+
(re.compile(r"\bstyle\s*=\s*[\"'][^\"']*(expression\s*\(|url\s*\(\s*javascript:)", re.I), "CSS expression/url()", 0.66),
|
|
135
|
+
(re.compile(r"@import\s+url\s*\(", re.I), "CSS @import vector", 0.45),
|
|
136
|
+
(re.compile(r"<!\[CDATA\[|\/\/\s*<\s*!\s*\[CDATA\[", re.I), "CDATA/comentarios para evasión", 0.48),
|
|
137
|
+
(re.compile(r"&#x[0-9a-fA-F]+;|&#\d+;", re.I), "Entidades HTML/encoding", 0.70),
|
|
138
|
+
(re.compile(r"%3C\s*script|%3Cscript%3E", re.I), "Tags URL-encoded", 0.68),
|
|
139
|
+
]
|
|
140
|
+
|
|
141
|
+
SENSITIVE_FIELDS = ["password", "csrfmiddlewaretoken", "token", "auth"]
|
|
142
|
+
SENSITIVE_DISCOUNT = 0.5
|
|
143
|
+
|
|
144
|
+
# ----------------------------
|
|
145
|
+
# Funciones criptográficas (derivación, AEAD, HMAC, hash)
|
|
146
|
+
# ----------------------------
|
|
147
|
+
def derive_key(label: bytes, context: bytes = b"") -> bytes:
|
|
148
|
+
salt = (label + context)[:16].ljust(16, b"\0")
|
|
149
|
+
try:
|
|
150
|
+
raw = hash_secret_raw(secret=MASTER_KEY if isinstance(MASTER_KEY, (bytes, bytearray)) else MASTER_KEY.encode(),
|
|
151
|
+
salt=salt,
|
|
152
|
+
time_cost=ARGON2_CONFIG["time_cost"],
|
|
153
|
+
memory_cost=ARGON2_CONFIG["memory_cost"],
|
|
154
|
+
parallelism=ARGON2_CONFIG["parallelism"],
|
|
155
|
+
hash_len=ARGON2_CONFIG["hash_len"],
|
|
156
|
+
type=ARGON2_CONFIG["type"])
|
|
157
|
+
hk = HKDF(algorithm=hashes.SHA256(), length=32, salt=salt, info=label + context)
|
|
158
|
+
return hk.derive(raw)
|
|
159
|
+
except Exception:
|
|
160
|
+
hk = HKDF(algorithm=hashes.SHA256(), length=32, salt=salt, info=label + context)
|
|
161
|
+
return hk.derive(MASTER_KEY if isinstance(MASTER_KEY, bytes) else MASTER_KEY.encode())
|
|
162
|
+
|
|
163
|
+
def aead_encrypt(plaintext: bytes, aad: bytes = b"", context: bytes = b"") -> Dict[str, bytes]:
|
|
164
|
+
key = derive_key(AEAD_LABEL, context)
|
|
165
|
+
if AEAD_CHOICE == "CHACHA20":
|
|
166
|
+
aead = ChaCha20Poly1305(key)
|
|
167
|
+
nonce = os.urandom(12)
|
|
168
|
+
ct = aead.encrypt(nonce, plaintext, aad)
|
|
169
|
+
return {"alg": "CHACHA20-POLY1305", "nonce": nonce, "ciphertext": ct}
|
|
170
|
+
else:
|
|
171
|
+
aead = AESGCM(key)
|
|
172
|
+
nonce = os.urandom(12)
|
|
173
|
+
ct = aead.encrypt(nonce, plaintext, aad)
|
|
174
|
+
return {"alg": "AES-GCM", "nonce": nonce, "ciphertext": ct}
|
|
175
|
+
|
|
176
|
+
def aead_decrypt(payload: Dict[str, bytes], aad: bytes = b"", context: bytes = b"") -> bytes:
|
|
177
|
+
key = derive_key(AEAD_LABEL, context)
|
|
178
|
+
alg = payload.get("alg", "AES-GCM")
|
|
179
|
+
nonce = payload.get("nonce")
|
|
180
|
+
ct = payload.get("ciphertext")
|
|
181
|
+
if not nonce or not ct:
|
|
182
|
+
raise ValueError("invalid payload for AEAD decrypt")
|
|
183
|
+
if alg.startswith("CHACHA20"):
|
|
184
|
+
aead = ChaCha20Poly1305(key)
|
|
185
|
+
return aead.decrypt(nonce, ct, aad)
|
|
186
|
+
else:
|
|
187
|
+
aead = AESGCM(key)
|
|
188
|
+
return aead.decrypt(nonce, ct, aad)
|
|
189
|
+
|
|
190
|
+
def compute_hmac(data: bytes, context: bytes = b"") -> bytes:
|
|
191
|
+
key = derive_key(HMAC_LABEL, context)
|
|
192
|
+
h = crypto_hmac.HMAC(key, hashes.SHA256())
|
|
193
|
+
h.update(data)
|
|
194
|
+
return h.finalize()
|
|
195
|
+
|
|
196
|
+
def verify_hmac(data: bytes, tag: bytes, context: bytes = b"") -> bool:
|
|
197
|
+
key = derive_key(HMAC_LABEL, context)
|
|
198
|
+
h = crypto_hmac.HMAC(key, hashes.SHA256())
|
|
199
|
+
h.update(data)
|
|
200
|
+
try:
|
|
201
|
+
h.verify(tag)
|
|
202
|
+
return True
|
|
203
|
+
except InvalidSignature:
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
def compute_hash(data: bytes) -> str:
|
|
207
|
+
if HASH_CHOICE == "SHA3":
|
|
208
|
+
h = hashes.Hash(hashes.SHA3_256())
|
|
209
|
+
else:
|
|
210
|
+
h = hashes.Hash(hashes.SHA256())
|
|
211
|
+
h.update(data)
|
|
212
|
+
return base64.b64encode(h.finalize()).decode()
|
|
213
|
+
|
|
214
|
+
# ----------------------------
|
|
215
|
+
# Función de saturación
|
|
216
|
+
# ----------------------------
|
|
217
|
+
SATURATION_C = getattr(settings, "XSS_DEFENSE_SATURATION_C", 1.5)
|
|
218
|
+
SATURATION_ALPHA = getattr(settings, "XSS_DEFENSE_SATURATION_ALPHA", 2.0)
|
|
219
|
+
|
|
220
|
+
def saturate_score(raw_score: float) -> float:
|
|
221
|
+
try:
|
|
222
|
+
x = float(raw_score)
|
|
223
|
+
alpha = float(SATURATION_ALPHA)
|
|
224
|
+
c = float(SATURATION_C)
|
|
225
|
+
return 1.0 / (1.0 + math.exp(-alpha * (x - c)))
|
|
226
|
+
except Exception:
|
|
227
|
+
return 0.0
|
|
228
|
+
|
|
229
|
+
# ----------------------------
|
|
230
|
+
# IP robusta
|
|
231
|
+
# ----------------------------
|
|
232
|
+
def _is_valid_ip(ip: str) -> bool:
|
|
233
|
+
try:
|
|
234
|
+
import ipaddress
|
|
235
|
+
ipaddress.ip_address(ip)
|
|
236
|
+
return True
|
|
237
|
+
except Exception:
|
|
238
|
+
return False
|
|
239
|
+
|
|
240
|
+
def get_client_ip(request) -> str:
|
|
241
|
+
xff = request.META.get("HTTP_X_FORWARDED_FOR")
|
|
242
|
+
if xff:
|
|
243
|
+
parts = [p.strip() for p in xff.split(",") if p.strip()]
|
|
244
|
+
if parts:
|
|
245
|
+
return parts[0]
|
|
246
|
+
for h in ("HTTP_X_REAL_IP", "HTTP_CF_CONNECTING_IP", "HTTP_CLIENT_IP"):
|
|
247
|
+
v = request.META.get(h)
|
|
248
|
+
if v and _is_valid_ip(v):
|
|
249
|
+
return v
|
|
250
|
+
return request.META.get("REMOTE_ADDR") or ""
|
|
251
|
+
|
|
252
|
+
# ----------------------------
|
|
253
|
+
# Extraer payload
|
|
254
|
+
# ----------------------------
|
|
255
|
+
def extract_body_as_map(request) -> Dict[str, Any]:
|
|
256
|
+
try:
|
|
257
|
+
ct = request.META.get("CONTENT_TYPE", "")
|
|
258
|
+
if "application/json" in ct:
|
|
259
|
+
raw = request.body.decode("utf-8") or "{}"
|
|
260
|
+
try:
|
|
261
|
+
data = json.loads(raw)
|
|
262
|
+
if isinstance(data, dict):
|
|
263
|
+
return data
|
|
264
|
+
return {"raw": raw}
|
|
265
|
+
except Exception:
|
|
266
|
+
return {"raw": raw}
|
|
267
|
+
try:
|
|
268
|
+
post = request.POST.dict()
|
|
269
|
+
if post:
|
|
270
|
+
return post
|
|
271
|
+
except Exception:
|
|
272
|
+
pass
|
|
273
|
+
raw = request.body.decode("utf-8", errors="ignore")
|
|
274
|
+
if raw:
|
|
275
|
+
return {"raw": raw}
|
|
276
|
+
except Exception:
|
|
277
|
+
pass
|
|
278
|
+
return {}
|
|
279
|
+
|
|
280
|
+
# ----------------------------
|
|
281
|
+
# Detect XSS en valor
|
|
282
|
+
# ----------------------------
|
|
283
|
+
def detect_xss_in_value(value: str, is_sensitive: bool = False) -> Tuple[float, List[str], List[str]]:
|
|
284
|
+
if not value:
|
|
285
|
+
return 0.0, [], []
|
|
286
|
+
score_total = 0.0
|
|
287
|
+
descripcion = []
|
|
288
|
+
matches = []
|
|
289
|
+
value = value.lower().strip()
|
|
290
|
+
if _BLEACH_AVAILABLE:
|
|
291
|
+
cleaned = bleach.clean(value, strip=True)
|
|
292
|
+
if cleaned != value:
|
|
293
|
+
score_total += 0.5
|
|
294
|
+
descripcion.append("Contenido alterado por sanitización (bleach)")
|
|
295
|
+
for patt, msg, weight in XSS_PATTERNS:
|
|
296
|
+
occ = len(patt.findall(value))
|
|
297
|
+
if occ > 0:
|
|
298
|
+
added = sum(weight * (0.5 ** i) for i in range(occ))
|
|
299
|
+
if is_sensitive:
|
|
300
|
+
added *= SENSITIVE_DISCOUNT
|
|
301
|
+
score_total += added
|
|
302
|
+
descripcion.append(msg)
|
|
303
|
+
matches.append(patt.pattern)
|
|
304
|
+
return round(score_total, 3), descripcion, matches
|
|
305
|
+
|
|
306
|
+
# ----------------------------
|
|
307
|
+
# Conversión a probabilidad
|
|
308
|
+
# ----------------------------
|
|
309
|
+
def weight_to_prob(w: float) -> float:
|
|
310
|
+
try:
|
|
311
|
+
q = 1.0 - math.exp(-max(w, 0.0))
|
|
312
|
+
return min(max(q, 0.0), 0.999999)
|
|
313
|
+
except Exception:
|
|
314
|
+
return min(max(w, 0.0), 0.999999)
|
|
315
|
+
|
|
316
|
+
def combine_probs(qs: List[float]) -> float:
|
|
317
|
+
prod = 1.0
|
|
318
|
+
for q in qs:
|
|
319
|
+
prod *= (1.0 - q)
|
|
320
|
+
return 1.0 - prod
|
|
321
|
+
|
|
322
|
+
# ----------------------------
|
|
323
|
+
# Firmar/cifrar cookies (integración cripto)
|
|
324
|
+
# ----------------------------
|
|
325
|
+
def sign_cookie_value(value: str, context: bytes = b"") -> str:
|
|
326
|
+
"""Firma un valor de cookie con HMAC-SHA256 para evitar alteraciones por XSS."""
|
|
327
|
+
data = value.encode("utf-8")
|
|
328
|
+
tag = compute_hmac(data, context)
|
|
329
|
+
return f"{value}.{base64.b64encode(tag).decode()}"
|
|
330
|
+
|
|
331
|
+
def verify_cookie_signature(signed_value: str, context: bytes = b"") -> str:
|
|
332
|
+
"""Verifica la firma de una cookie y retorna el valor original si es válido."""
|
|
333
|
+
try:
|
|
334
|
+
value, tag_b64 = signed_value.rsplit(".", 1)
|
|
335
|
+
tag = base64.b64decode(tag_b64)
|
|
336
|
+
if verify_hmac(value.encode("utf-8"), tag, context):
|
|
337
|
+
return value
|
|
338
|
+
else:
|
|
339
|
+
raise ValueError("Invalid signature")
|
|
340
|
+
except Exception:
|
|
341
|
+
raise ValueError("Invalid signed cookie")
|
|
342
|
+
|
|
343
|
+
def encrypt_cookie_value(value: str, context: bytes = b"") -> str:
|
|
344
|
+
"""Cifra un valor de cookie sensible con AEAD."""
|
|
345
|
+
enc = aead_encrypt(value.encode("utf-8"), context=context)
|
|
346
|
+
return base64.b64encode(json.dumps(enc).encode()).decode()
|
|
347
|
+
|
|
348
|
+
def decrypt_cookie_value(encrypted_value: str, context: bytes = b"") -> str:
|
|
349
|
+
"""Descifra un valor de cookie."""
|
|
350
|
+
try:
|
|
351
|
+
enc = json.loads(base64.b64decode(encrypted_value))
|
|
352
|
+
plaintext = aead_decrypt(enc, context=context)
|
|
353
|
+
return plaintext.decode("utf-8")
|
|
354
|
+
except Exception:
|
|
355
|
+
raise ValueError("Invalid encrypted cookie")
|
|
356
|
+
|
|
357
|
+
# ----------------------------
|
|
358
|
+
# Funciones de cache para bloqueo (similar a SQLi, pero con prefijo XSS_)
|
|
359
|
+
# ----------------------------
|
|
360
|
+
def cache_block_ip_with_backoff(ip: str):
|
|
361
|
+
if not ip:
|
|
362
|
+
return 0, 0
|
|
363
|
+
level_key = f"{XSS_CACHE_BLOCK_KEY_PREFIX}{ip}:level"
|
|
364
|
+
level = cache.get(level_key, 0) or 0
|
|
365
|
+
level = int(level) + 1
|
|
366
|
+
cache.set(level_key, level, timeout=60 * 60 * 24 * 7)
|
|
367
|
+
durations = XSS_DEFAULT_BACKOFF_LEVELS
|
|
368
|
+
idx = min(level, len(durations) - 1)
|
|
369
|
+
timeout = durations[idx]
|
|
370
|
+
cache.set(f"{XSS_CACHE_BLOCK_KEY_PREFIX}{ip}", True, timeout=timeout)
|
|
371
|
+
return level, timeout
|
|
372
|
+
|
|
373
|
+
def is_ip_blocked(ip: str) -> bool:
|
|
374
|
+
if not ip:
|
|
375
|
+
return False
|
|
376
|
+
return bool(cache.get(f"{XSS_CACHE_BLOCK_KEY_PREFIX}{ip}"))
|
|
377
|
+
|
|
378
|
+
def incr_ip_counter(ip: str) -> int:
|
|
379
|
+
if not ip:
|
|
380
|
+
return 0
|
|
381
|
+
key = f"{XSS_CACHE_COUNTER_KEY_PREFIX}{ip}"
|
|
382
|
+
current = cache.get(key, 0)
|
|
383
|
+
try:
|
|
384
|
+
current = int(current)
|
|
385
|
+
except Exception:
|
|
386
|
+
current = 0
|
|
387
|
+
current += 1
|
|
388
|
+
cache.set(key, current, timeout=XSS_COUNTER_WINDOW)
|
|
389
|
+
return current
|
|
390
|
+
|
|
391
|
+
# ----------------------------
|
|
392
|
+
# Registro cifrado de eventos (similar a SQLi para asegurar registro)
|
|
393
|
+
# ----------------------------
|
|
394
|
+
def record_xss_event(event: dict) -> None:
|
|
395
|
+
try:
|
|
396
|
+
ts = int(time.time())
|
|
397
|
+
# cifrar payload si existe
|
|
398
|
+
if "payload" in event and event["payload"]:
|
|
399
|
+
try:
|
|
400
|
+
ctx = f"{event.get('ip','')}-{ts}".encode()
|
|
401
|
+
enc = aead_encrypt(json.dumps(event["payload"], ensure_ascii=False).encode("utf-8"), context=ctx)
|
|
402
|
+
htag = compute_hmac(enc["ciphertext"], context=ctx)
|
|
403
|
+
event["_payload_encrypted"] = {
|
|
404
|
+
"alg": enc["alg"],
|
|
405
|
+
"nonce": base64.b64encode(enc["nonce"]).decode(),
|
|
406
|
+
"ciphertext": base64.b64encode(enc["ciphertext"]).decode(),
|
|
407
|
+
"hmac": base64.b64encode(htag).decode(),
|
|
408
|
+
}
|
|
409
|
+
del event["payload"] # no almacenar plaintext
|
|
410
|
+
except Exception:
|
|
411
|
+
# si falla, simplemente no incluimos payload
|
|
412
|
+
event.pop("payload", None)
|
|
413
|
+
key = f"xss_event:{ts}:{event.get('ip', '')}"
|
|
414
|
+
cache.set(key, json.dumps(event, ensure_ascii=False), timeout=60 * 60 * 24)
|
|
415
|
+
except Exception:
|
|
416
|
+
logger.exception("record_xss_event failed")
|
|
417
|
+
|
|
418
|
+
# ----------------------------
|
|
419
|
+
# Middleware XSS con cripto integrado (ajustado para registro similar a SQLi y chequeo de bloqueo inicial)
|
|
420
|
+
# ----------------------------
|
|
421
|
+
class XSSDefenseCryptoMiddleware(MiddlewareMixin):
|
|
422
|
+
def process_request(self, request):
|
|
423
|
+
client_ip = get_client_ip(request)
|
|
424
|
+
|
|
425
|
+
# Chequear bloqueo inicial (similar a SQLi)
|
|
426
|
+
if is_ip_blocked(client_ip):
|
|
427
|
+
warning_message = (
|
|
428
|
+
"Acceso denegado. Su dirección IP y actividades han sido registradas y monitoreadas. "
|
|
429
|
+
"Continuar con estos intentos podría resultar en exposición pública, bloqueos permanentes o acciones legales. "
|
|
430
|
+
"Recomendamos detenerse inmediatamente para evitar riesgos mayores."
|
|
431
|
+
)
|
|
432
|
+
logger.warning(f"[XSSBlock:Persistent] IP={client_ip} - Intento persistente de acceso bloqueado. Mensaje enviado.")
|
|
433
|
+
return HttpResponseForbidden(warning_message)
|
|
434
|
+
|
|
435
|
+
trusted_ips: List[str] = getattr(settings, "XSS_DEFENSE_TRUSTED_IPS", [])
|
|
436
|
+
if client_ip in trusted_ips:
|
|
437
|
+
return None
|
|
438
|
+
excluded_paths: List[str] = getattr(settings, "XSS_DEFENSE_EXCLUDED_PATHS", [])
|
|
439
|
+
if any(request.path.startswith(p) for p in excluded_paths):
|
|
440
|
+
return None
|
|
441
|
+
|
|
442
|
+
data = extract_body_as_map(request)
|
|
443
|
+
qs = request.META.get("QUERY_STRING", "")
|
|
444
|
+
if qs:
|
|
445
|
+
data["_query_string"] = qs
|
|
446
|
+
if not data:
|
|
447
|
+
return None
|
|
448
|
+
|
|
449
|
+
total_score = 0.0
|
|
450
|
+
all_descriptions: List[str] = []
|
|
451
|
+
global_prob_list: List[float] = []
|
|
452
|
+
payload_summary = []
|
|
453
|
+
|
|
454
|
+
if isinstance(data, dict):
|
|
455
|
+
for key, value in data.items():
|
|
456
|
+
is_sensitive = key.lower() in SENSITIVE_FIELDS
|
|
457
|
+
vtext = value
|
|
458
|
+
if isinstance(value, (dict, list)):
|
|
459
|
+
try:
|
|
460
|
+
vtext = json.dumps(value, ensure_ascii=False)
|
|
461
|
+
except Exception:
|
|
462
|
+
vtext = str(value)
|
|
463
|
+
else:
|
|
464
|
+
vtext = str(value or "")
|
|
465
|
+
s, descs, matches = detect_xss_in_value(vtext, is_sensitive)
|
|
466
|
+
total_score += s
|
|
467
|
+
all_descriptions.extend(descs)
|
|
468
|
+
for m in matches:
|
|
469
|
+
q = weight_to_prob(s)
|
|
470
|
+
global_prob_list.append(q)
|
|
471
|
+
if s > 0:
|
|
472
|
+
payload_summary.append({"field": key, "snippet": vtext[:300], "sensitive": is_sensitive})
|
|
473
|
+
else:
|
|
474
|
+
raw = str(data)
|
|
475
|
+
s, descs, matches = detect_xss_in_value(raw)
|
|
476
|
+
total_score += s
|
|
477
|
+
all_descriptions.extend(descs)
|
|
478
|
+
for m in matches:
|
|
479
|
+
q = weight_to_prob(s)
|
|
480
|
+
global_prob_list.append(q)
|
|
481
|
+
if s > 0:
|
|
482
|
+
payload_summary.append({"field": "raw", "snippet": raw[:500], "sensitive": False})
|
|
483
|
+
|
|
484
|
+
if total_score == 0:
|
|
485
|
+
return None
|
|
486
|
+
|
|
487
|
+
p_attack = combine_probs(global_prob_list) if global_prob_list else 0.0
|
|
488
|
+
s_norm = saturate_score(total_score)
|
|
489
|
+
url = request.build_absolute_uri()
|
|
490
|
+
payload_for_request = json.dumps(payload_summary, ensure_ascii=False)[:2000]
|
|
491
|
+
|
|
492
|
+
logger.warning(
|
|
493
|
+
"[XSSDetect] IP=%s URL=%s ScoreRaw=%.3f ScoreNorm=%.3f Prob=%.3f Desc=%s",
|
|
494
|
+
client_ip, url, total_score, s_norm, p_attack, all_descriptions
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
# Registrar el evento de manera cifrada para auditoría (similar a SQLi)
|
|
498
|
+
try:
|
|
499
|
+
record_xss_event({
|
|
500
|
+
"ts": int(time.time()),
|
|
501
|
+
"ip": client_ip,
|
|
502
|
+
"score_raw": total_score,
|
|
503
|
+
"score_norm": s_norm,
|
|
504
|
+
"prob": p_attack,
|
|
505
|
+
"desc": all_descriptions,
|
|
506
|
+
"url": url,
|
|
507
|
+
"payload": payload_summary, # se cifrará en record_xss_event
|
|
508
|
+
})
|
|
509
|
+
except Exception:
|
|
510
|
+
logger.exception("failed to record XSS event")
|
|
511
|
+
|
|
512
|
+
# Asignar información de ataque al request para uso posterior (e.g., en vistas o middleware de respuesta)
|
|
513
|
+
request.xss_attack_info = {
|
|
514
|
+
"ip": client_ip,
|
|
515
|
+
"tipos": ["XSS"],
|
|
516
|
+
"descripcion": all_descriptions,
|
|
517
|
+
"payload": payload_for_request,
|
|
518
|
+
"score_raw": total_score,
|
|
519
|
+
"score_norm": s_norm,
|
|
520
|
+
"prob": p_attack,
|
|
521
|
+
"url": url,
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
# Políticas de bloqueo (similar a SQLi, pero ajustadas para XSS)
|
|
525
|
+
if s_norm >= XSS_NORM_THRESHOLDS["HIGH"]:
|
|
526
|
+
level, timeout = cache_block_ip_with_backoff(client_ip)
|
|
527
|
+
logger.error(f"[XSSBlock] IP={client_ip} ScoreRaw={total_score:.3f} ScoreNorm={s_norm:.3f} URL={url}")
|
|
528
|
+
request.xss_attack_info.update({"blocked": True, "action": "block", "block_timeout": timeout, "block_level": level})
|
|
529
|
+
# Setear flag para bloqueo
|
|
530
|
+
request.xss_block = True
|
|
531
|
+
request.xss_block_response = HttpResponseForbidden("Request blocked by XSS defense")
|
|
532
|
+
return None
|
|
533
|
+
elif s_norm >= XSS_NORM_THRESHOLDS["MEDIUM"]:
|
|
534
|
+
logger.warning(f"[XSSAlert] IP={client_ip} ScoreRaw={total_score:.3f} ScoreNorm={s_norm:.3f} - applying counter/challenge")
|
|
535
|
+
count = incr_ip_counter(client_ip)
|
|
536
|
+
request.xss_attack_info.update({"blocked": False, "action": "alert", "counter": count})
|
|
537
|
+
if count >= XSS_COUNTER_THRESHOLD:
|
|
538
|
+
level, timeout = cache_block_ip_with_backoff(client_ip)
|
|
539
|
+
cache.set(f"{XSS_CACHE_COUNTER_KEY_PREFIX}{client_ip}", 0, timeout=XSS_COUNTER_WINDOW)
|
|
540
|
+
logger.error(f"[XSSAutoBlock] IP={client_ip} reached counter={count} -> blocking for {timeout}s")
|
|
541
|
+
request.xss_attack_info.update({"blocked": True, "action": "auto_block", "block_timeout": timeout, "block_level": level})
|
|
542
|
+
# Setear flag para bloqueo
|
|
543
|
+
request.xss_block = True
|
|
544
|
+
request.xss_block_response = HttpResponseForbidden("Request blocked by XSS defense (auto block)")
|
|
545
|
+
return None
|
|
546
|
+
if getattr(settings, "XSS_DEFENSE_USE_CHALLENGE", False):
|
|
547
|
+
# Setear flag para challenge
|
|
548
|
+
request.xss_challenge = True
|
|
549
|
+
request.xss_challenge_response = HttpResponse("Challenge required", status=403)
|
|
550
|
+
request.xss_challenge_response["X-XSS-Challenge"] = "captcha"
|
|
551
|
+
return None
|
|
552
|
+
return None
|
|
553
|
+
elif s_norm >= XSS_NORM_THRESHOLDS["LOW"]:
|
|
554
|
+
logger.info(f"[XSSMonitor] IP={client_ip} ScoreRaw={total_score:.3f} ScoreNorm={s_norm:.3f} - monitored")
|
|
555
|
+
request.xss_attack_info.update({"blocked": False, "action": "monitor"})
|
|
556
|
+
return None
|
|
557
|
+
return None
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
# =====================================================
|
|
561
|
+
# === INFORMACIÓN EXTRA ===
|
|
562
|
+
# =====================================================
|
|
563
|
+
"""
|
|
564
|
+
Algoritmos relacionados:
|
|
565
|
+
- Se recomienda almacenar los payloads XSS cifrados con AES-GCM
|
|
566
|
+
para confidencialidad e integridad.
|
|
567
|
+
|
|
568
|
+
Contribución a fórmula de amenaza S:
|
|
569
|
+
S_xss = w_xss * detecciones_xss
|
|
570
|
+
Ejemplo: S_xss = 0.3 * 2 = 0.6
|
|
571
|
+
|
|
572
|
+
Notas sobre implementación de algoritmos de seguridad:
|
|
573
|
+
- HMAC-SHA256: Usado para firmar tokens/cookies (sign_cookie_value, verify_cookie_signature).
|
|
574
|
+
- SHA-256/SHA-3: Usado para hashes de contenido (compute_hash).
|
|
575
|
+
- AES-GCM/ChaCha20-Poly1305: Usado para cifrar cookies sensibles (encrypt_cookie_value, decrypt_cookie_value).
|
|
576
|
+
- HKDF: Usado para derivar claves seguras (derive_key).
|
|
577
|
+
- Argon2id: Usado para derivar claves con resistencia a ataques de fuerza bruta (derive_key).
|
|
578
|
+
- TLS 1.3: Recomendado para configurar en el servidor web (e.g., Nginx/Apache) para proteger datos en tránsito.
|
|
579
|
+
Ejemplo configuración Nginx:
|
|
580
|
+
server {
|
|
581
|
+
listen 443 ssl http2;
|
|
582
|
+
ssl_protocols TLSv1.3;
|
|
583
|
+
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305;
|
|
584
|
+
# ... otras configuraciones SSL
|
|
585
|
+
}
|
|
586
|
+
Esto asegura que las comunicaciones sean seguras y eviten que XSS robe datos en tránsito.
|
|
587
|
+
|
|
588
|
+
Para usar en producción:
|
|
589
|
+
- Configura XSS_DEFENSE_MASTER_KEY en settings.py como base64 de una clave segura de 32 bytes.
|
|
590
|
+
- Ajusta umbrales y configuraciones según necesidades.
|
|
591
|
+
- Integra con CSP (Content Security Policy) en headers de respuesta para mayor protección.
|
|
592
|
+
"""
|
|
593
|
+
|
|
594
|
+
# =====================================================
|
|
595
|
+
# === INFORMACIÓN EXTRA ===
|
|
596
|
+
# =====================================================
|
|
597
|
+
"""
|
|
598
|
+
Algoritmos relacionados:
|
|
599
|
+
- Se recomienda almacenar los payloads XSS cifrados con AES-GCM
|
|
600
|
+
para confidencialidad e integridad.
|
|
601
|
+
|
|
602
|
+
Contribución a fórmula de amenaza S:
|
|
603
|
+
S_xss = w_xss * detecciones_xss
|
|
604
|
+
Ejemplo: S_xss = 0.3 * 2 = 0.6
|
|
605
|
+
|
|
606
|
+
Notas sobre implementación de algoritmos de seguridad:
|
|
607
|
+
- HMAC-SHA256: Usado para firmar tokens/cookies (sign_cookie_value, verify_cookie_signature).
|
|
608
|
+
- SHA-256/SHA-3: Usado para hashes de contenido (compute_hash).
|
|
609
|
+
- AES-GCM/ChaCha20-Poly1305: Usado para cifrar cookies sensibles (encrypt_cookie_value, decrypt_cookie_value).
|
|
610
|
+
- HKDF: Usado para derivar claves seguras (derive_key).
|
|
611
|
+
- Argon2id: Usado para derivar claves con resistencia a ataques de fuerza bruta (derive_key).
|
|
612
|
+
- TLS 1.3: Recomendado para configurar en el servidor web (e.g., Nginx/Apache) para proteger datos en tránsito.
|
|
613
|
+
Ejemplo configuración Nginx:
|
|
614
|
+
server {
|
|
615
|
+
listen 443 ssl http2;
|
|
616
|
+
ssl_protocols TLSv1.3;
|
|
617
|
+
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305;
|
|
618
|
+
# ... otras configuraciones SSL
|
|
619
|
+
}
|
|
620
|
+
Esto asegura que las comunicaciones sean seguras y eviten que XSS robe datos en tránsito.
|
|
621
|
+
|
|
622
|
+
Para usar en producción:
|
|
623
|
+
- Configura XSS_DEFENSE_MASTER_KEY en settings.py como base64 de una clave segura de 32 bytes.
|
|
624
|
+
- Ajusta umbrales y configuraciones según necesidades.
|
|
625
|
+
- Integra con CSP (Content Security Policy) en headers de respuesta para mayor protección.
|
|
626
|
+
"""
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Middleware base para frameworks web (Django/Flask/FastAPI)
|
|
3
|
+
"""
|
|
4
|
+
from ..detectores.detector_sql import detectar_inyeccion_sql
|
|
5
|
+
from ..detectores.detector_xss import detectar_xss
|
|
6
|
+
|
|
7
|
+
def middleware_proteccion(request):
|
|
8
|
+
# Simulación de protección de entrada
|
|
9
|
+
if detectar_inyeccion_sql(request.get("query", "")):
|
|
10
|
+
return {"error": "SQL Injection detectado"}
|
|
11
|
+
if detectar_xss(request.get("input", "")):
|
|
12
|
+
return {"error": "XSS detectado"}
|
|
13
|
+
return {"ok": True}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fórmula compuesta S para puntuar la amenaza global
|
|
3
|
+
"""
|
|
4
|
+
def calcular_puntuacion(detecciones_sql=0, detecciones_xss=0, intentos_csrf=0,
|
|
5
|
+
procesos_keylogger=0, tasa_dos=0,
|
|
6
|
+
w_sql=1.5, w_xss=1.2, w_csrf=1.0, w_keylogger=2.0, w_dos=2.5,
|
|
7
|
+
limite_dos=100) -> float:
|
|
8
|
+
S = (
|
|
9
|
+
w_sql * detecciones_sql +
|
|
10
|
+
w_xss * detecciones_xss +
|
|
11
|
+
w_csrf * intentos_csrf +
|
|
12
|
+
w_keylogger * procesos_keylogger +
|
|
13
|
+
w_dos * (tasa_dos / limite_dos)
|
|
14
|
+
)
|
|
15
|
+
return S
|