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,25 @@
|
|
|
1
|
+
""" Funciones principales """
|
|
2
|
+
# GuardianUnivalle_Benito_Yucra/__init__.py
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Paquete principal de GuardianUnivalle.
|
|
6
|
+
Incluye módulos de criptografía, detección de ataques, mitigación, auditoría y puntuación de amenazas.
|
|
7
|
+
"""
|
|
8
|
+
from . import criptografia
|
|
9
|
+
from . import detectores
|
|
10
|
+
from . import mitigacion
|
|
11
|
+
from . import auditoria
|
|
12
|
+
from . import puntuacion
|
|
13
|
+
from . import middleware_web
|
|
14
|
+
from . import utilidades
|
|
15
|
+
|
|
16
|
+
def protect_app():
|
|
17
|
+
"""
|
|
18
|
+
Activa todas las protecciones de seguridad de forma automática.
|
|
19
|
+
"""
|
|
20
|
+
print("🔒 GuardianUnivalle-Benito-Yucra: Seguridad activada")
|
|
21
|
+
# Aquí podríamos llamar funciones automáticamente si queremos
|
|
22
|
+
# scan_malware()
|
|
23
|
+
# rate_limiter()
|
|
24
|
+
# sanitize_input()
|
|
25
|
+
# check_csrf()
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import datetime
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
LOG_FILE = "auditoria_guardian.log"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def registrar_evento(
|
|
9
|
+
tipo: str,
|
|
10
|
+
descripcion: str = "",
|
|
11
|
+
severidad: str = "MEDIA",
|
|
12
|
+
extra: dict | None = None,
|
|
13
|
+
):
|
|
14
|
+
try:
|
|
15
|
+
evento = {
|
|
16
|
+
"fecha": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
17
|
+
"tipo": tipo,
|
|
18
|
+
"descripcion": descripcion,
|
|
19
|
+
"severidad": severidad,
|
|
20
|
+
"extra": extra or {},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# ✅ Crear carpeta solo si hay directorio en la ruta
|
|
24
|
+
log_dir = os.path.dirname(LOG_FILE)
|
|
25
|
+
if log_dir:
|
|
26
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
27
|
+
|
|
28
|
+
with open(LOG_FILE, "a", encoding="utf-8") as f:
|
|
29
|
+
f.write(json.dumps(evento, ensure_ascii=False) + "\n")
|
|
30
|
+
|
|
31
|
+
except Exception as e:
|
|
32
|
+
print(f"[Auditoría] Error al registrar evento: {e}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def generar_reporte() -> str:
|
|
36
|
+
"""Devuelve todo el contenido del archivo de auditoría."""
|
|
37
|
+
if not os.path.exists(LOG_FILE):
|
|
38
|
+
return "No hay registros aún."
|
|
39
|
+
with open(LOG_FILE, "r", encoding="utf-8") as f:
|
|
40
|
+
return f.read()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cifrado simétrico autenticado: AES-GCM y ChaCha20-Poly1305
|
|
3
|
+
"""
|
|
4
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM, ChaCha20Poly1305
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
def cifrar_aes_gcm(mensaje: bytes, clave: bytes) -> dict:
|
|
8
|
+
aes = AESGCM(clave)
|
|
9
|
+
nonce = os.urandom(12)
|
|
10
|
+
ciphertext = aes.encrypt(nonce, mensaje, None)
|
|
11
|
+
return {"nonce": nonce, "ciphertext": ciphertext}
|
|
12
|
+
|
|
13
|
+
def descifrar_aes_gcm(cipher: dict, clave: bytes) -> bytes:
|
|
14
|
+
aes = AESGCM(clave)
|
|
15
|
+
return aes.decrypt(cipher["nonce"], cipher["ciphertext"], None)
|
|
16
|
+
|
|
17
|
+
def cifrar_chacha20(mensaje: bytes, clave: bytes) -> dict:
|
|
18
|
+
cipher = ChaCha20Poly1305(clave)
|
|
19
|
+
nonce = os.urandom(12)
|
|
20
|
+
ciphertext = cipher.encrypt(nonce, mensaje, None)
|
|
21
|
+
return {"nonce": nonce, "ciphertext": ciphertext}
|
|
22
|
+
|
|
23
|
+
def descifrar_chacha20(cipher: dict, clave: bytes) -> bytes:
|
|
24
|
+
cipher_obj = ChaCha20Poly1305(clave)
|
|
25
|
+
return cipher_obj.decrypt(cipher["nonce"], cipher["ciphertext"], None)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Gestión de intercambio de claves con ECDH y derivación HKDF.
|
|
3
|
+
"""
|
|
4
|
+
from cryptography.hazmat.primitives.asymmetric import ec
|
|
5
|
+
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
|
6
|
+
from cryptography.hazmat.primitives import hashes
|
|
7
|
+
|
|
8
|
+
def generar_claves_ecdh():
|
|
9
|
+
"""Genera clave privada y pública ECDH"""
|
|
10
|
+
clave_privada = ec.generate_private_key(ec.SECP384R1())
|
|
11
|
+
clave_publica = clave_privada.public_key()
|
|
12
|
+
return clave_privada, clave_publica
|
|
13
|
+
|
|
14
|
+
def derivar_clave_secreta(clave_privada, clave_publica):
|
|
15
|
+
"""Deriva una clave compartida usando HKDF"""
|
|
16
|
+
shared_key = clave_privada.exchange(ec.ECDH(), clave_publica)
|
|
17
|
+
derived_key = HKDF(
|
|
18
|
+
algorithm=hashes.SHA256(),
|
|
19
|
+
length=32,
|
|
20
|
+
salt=None,
|
|
21
|
+
info=b'guardianclave'
|
|
22
|
+
).derive(shared_key)
|
|
23
|
+
return derived_key
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Wrappers para derivación de claves segura: PBKDF2 y Argon2
|
|
3
|
+
"""
|
|
4
|
+
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
5
|
+
from argon2 import PasswordHasher
|
|
6
|
+
from cryptography.hazmat.primitives import hashes
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
def pbkdf2_derivar_clave(password: str, salt: bytes = None) -> bytes:
|
|
10
|
+
"""Deriva clave usando PBKDF2"""
|
|
11
|
+
salt = salt or os.urandom(16)
|
|
12
|
+
kdf = PBKDF2HMAC(
|
|
13
|
+
algorithm=hashes.SHA256(),
|
|
14
|
+
length=32,
|
|
15
|
+
salt=salt,
|
|
16
|
+
iterations=100_000,
|
|
17
|
+
)
|
|
18
|
+
return kdf.derive(password.encode()), salt
|
|
19
|
+
|
|
20
|
+
def argon2_derivar_clave(password: str) -> str:
|
|
21
|
+
"""Deriva clave usando Argon2"""
|
|
22
|
+
ph = PasswordHasher()
|
|
23
|
+
return ph.hash(password)
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import secrets
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
import json
|
|
6
|
+
import base64
|
|
7
|
+
import os
|
|
8
|
+
import time
|
|
9
|
+
from typing import List, Dict, Any
|
|
10
|
+
from urllib.parse import urlparse
|
|
11
|
+
from django.conf import settings
|
|
12
|
+
from django.utils.deprecation import MiddlewareMixin
|
|
13
|
+
from django.core.cache import cache
|
|
14
|
+
|
|
15
|
+
# cryptography & argon2
|
|
16
|
+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM, ChaCha20Poly1305
|
|
17
|
+
from cryptography.hazmat.primitives import hashes, hmac as crypto_hmac
|
|
18
|
+
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
|
19
|
+
from cryptography.exceptions import InvalidSignature
|
|
20
|
+
from argon2.low_level import hash_secret_raw, Type as Argon2Type
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger("csrfdefense")
|
|
23
|
+
logger.setLevel(logging.INFO)
|
|
24
|
+
if not logger.handlers:
|
|
25
|
+
handler = logging.StreamHandler()
|
|
26
|
+
handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
|
|
27
|
+
logger.addHandler(handler)
|
|
28
|
+
|
|
29
|
+
STATE_CHANGING_METHODS = {"POST", "PUT", "PATCH", "DELETE"}
|
|
30
|
+
CSRF_HEADER_NAMES = ("HTTP_X_CSRFTOKEN", "HTTP_X_CSRF_TOKEN")
|
|
31
|
+
CSRF_COOKIE_NAME = getattr(settings, "CSRF_COOKIE_NAME", "csrftoken")
|
|
32
|
+
POST_FIELD_NAME = "csrfmiddlewaretoken"
|
|
33
|
+
|
|
34
|
+
# Patrón de Content-Type sospechoso - EXPANDIDO
|
|
35
|
+
SUSPICIOUS_CT_PATTERNS = [
|
|
36
|
+
re.compile(r"text/plain", re.I),
|
|
37
|
+
re.compile(r"application/x-www-form-urlencoded", re.I),
|
|
38
|
+
re.compile(r"multipart/form-data", re.I),
|
|
39
|
+
re.compile(r"application/json", re.I),
|
|
40
|
+
re.compile(r"text/html", re.I), # Agregado para HTML CSRF
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
# Parámetros sensibles típicos de CSRF - EXPANDIDO
|
|
44
|
+
SENSITIVE_PARAMS = [
|
|
45
|
+
"password", "csrfmiddlewaretoken", "token", "amount", "transfer", "delete", "update", "action", "email", "username"
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
# Campos sensibles: ANALIZAMOS COMPLETAMENTE SIN DESCUENTO PARA ROBUSTEZ MÁXIMA
|
|
49
|
+
SENSITIVE_FIELDS = ["password", "csrfmiddlewaretoken", "token", "auth", "email", "username"]
|
|
50
|
+
|
|
51
|
+
CSRF_DEFENSE_MIN_SIGNALS = getattr(settings, "CSRF_DEFENSE_MIN_SIGNALS", 1)
|
|
52
|
+
CSRF_DEFENSE_EXCLUDED_API_PREFIXES = getattr(settings, "CSRF_DEFENSE_EXCLUDED_API_PREFIXES", ["/api/"])
|
|
53
|
+
|
|
54
|
+
# PATRONES EXPANDIDOS PARA ANÁLISIS DE PAYLOAD EN TODOS LOS CAMPOS (SIN DESCUENTO)
|
|
55
|
+
CSRF_PAYLOAD_PATTERNS = [
|
|
56
|
+
(re.compile(r"<script[^>]*>.*?</script>", re.I | re.S), "Script tag en payload", 0.9),
|
|
57
|
+
(re.compile(r"javascript\s*:", re.I), "URI javascript: en payload", 0.8),
|
|
58
|
+
(re.compile(r"http[s]?://[^\s]+", re.I), "URL externa en payload", 0.7),
|
|
59
|
+
(re.compile(r"eval\s*\$", re.I), "eval() en payload", 1.0),
|
|
60
|
+
(re.compile(r"document\.cookie", re.I), "Acceso a cookie en payload", 0.9),
|
|
61
|
+
(re.compile(r"innerHTML\s*=", re.I), "Manipulación DOM innerHTML", 0.8),
|
|
62
|
+
(re.compile(r"XMLHttpRequest", re.I), "XHR en payload", 0.7),
|
|
63
|
+
(re.compile(r"fetch\s*\$", re.I), "fetch() en payload", 0.7),
|
|
64
|
+
(re.compile(r"&#x[0-9a-fA-F]+;", re.I), "Entidades HTML en payload", 0.6),
|
|
65
|
+
(re.compile(r"%3Cscript", re.I), "Script URL-encoded en payload", 0.8),
|
|
66
|
+
(re.compile(r"on\w+\s*=", re.I), "Eventos on* en payload", 0.7),
|
|
67
|
+
(re.compile(r"alert\s*\$", re.I), "alert() en payload (prueba)", 0.5),
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
# ----------------------------
|
|
71
|
+
# Configuraciones criptográficas (similar a XSS/SQLi)
|
|
72
|
+
# ----------------------------
|
|
73
|
+
MASTER_KEY_B64 = getattr(settings, "CSRF_DEFENSE_MASTER_KEY", None)
|
|
74
|
+
if not MASTER_KEY_B64:
|
|
75
|
+
MASTER_KEY = os.urandom(32)
|
|
76
|
+
else:
|
|
77
|
+
try:
|
|
78
|
+
MASTER_KEY = base64.b64decode(MASTER_KEY_B64)
|
|
79
|
+
except Exception:
|
|
80
|
+
MASTER_KEY = MASTER_KEY_B64.encode() if isinstance(MASTER_KEY_B64, str) else MASTER_KEY_B64
|
|
81
|
+
|
|
82
|
+
AEAD_CHOICE = getattr(settings, "CSRF_DEFENSE_AEAD", "AESGCM").upper() # AESGCM o CHACHA20
|
|
83
|
+
ARGON2_CONFIG = getattr(settings, "CSRF_DEFENSE_ARGON2", {
|
|
84
|
+
"time_cost": 2,
|
|
85
|
+
"memory_cost": 65536,
|
|
86
|
+
"parallelism": 1,
|
|
87
|
+
"hash_len": 32,
|
|
88
|
+
"type": Argon2Type.ID,
|
|
89
|
+
})
|
|
90
|
+
HMAC_LABEL = getattr(settings, "CSRF_HMAC_LABEL", b"csrfdefense-hmac")
|
|
91
|
+
AEAD_LABEL = getattr(settings, "CSRF_AEAD_LABEL", b"csrfdefense-aead")
|
|
92
|
+
HASH_CHOICE = getattr(settings, "CSRF_DEFENSE_HASH", "SHA256").upper() # SHA256 o SHA3
|
|
93
|
+
|
|
94
|
+
# ----------------------------
|
|
95
|
+
# Funciones criptográficas (derivación, AEAD, HMAC, hash)
|
|
96
|
+
# ----------------------------
|
|
97
|
+
def derive_key(label: bytes, context: bytes = b"") -> bytes:
|
|
98
|
+
salt = (label + context)[:16].ljust(16, b"\0")
|
|
99
|
+
try:
|
|
100
|
+
raw = hash_secret_raw(secret=MASTER_KEY if isinstance(MASTER_KEY, (bytes, bytearray)) else MASTER_KEY.encode(),
|
|
101
|
+
salt=salt,
|
|
102
|
+
time_cost=ARGON2_CONFIG["time_cost"],
|
|
103
|
+
memory_cost=ARGON2_CONFIG["memory_cost"],
|
|
104
|
+
parallelism=ARGON2_CONFIG["parallelism"],
|
|
105
|
+
hash_len=ARGON2_CONFIG["hash_len"],
|
|
106
|
+
type=ARGON2_CONFIG["type"])
|
|
107
|
+
hk = HKDF(algorithm=hashes.SHA256(), length=32, salt=salt, info=label + context)
|
|
108
|
+
return hk.derive(raw)
|
|
109
|
+
except Exception:
|
|
110
|
+
hk = HKDF(algorithm=hashes.SHA256(), length=32, salt=salt, info=label + context)
|
|
111
|
+
return hk.derive(MASTER_KEY if isinstance(MASTER_KEY, bytes) else MASTER_KEY.encode())
|
|
112
|
+
|
|
113
|
+
def aead_encrypt(plaintext: bytes, aad: bytes = b"", context: bytes = b"") -> Dict[str, bytes]:
|
|
114
|
+
key = derive_key(AEAD_LABEL, context)
|
|
115
|
+
if AEAD_CHOICE == "CHACHA20":
|
|
116
|
+
aead = ChaCha20Poly1305(key)
|
|
117
|
+
nonce = os.urandom(12)
|
|
118
|
+
ct = aead.encrypt(nonce, plaintext, aad)
|
|
119
|
+
return {"alg": "CHACHA20-POLY1305", "nonce": nonce, "ciphertext": ct}
|
|
120
|
+
else:
|
|
121
|
+
aead = AESGCM(key)
|
|
122
|
+
nonce = os.urandom(12)
|
|
123
|
+
ct = aead.encrypt(nonce, plaintext, aad)
|
|
124
|
+
return {"alg": "AES-GCM", "nonce": nonce, "ciphertext": ct}
|
|
125
|
+
|
|
126
|
+
def aead_decrypt(payload: Dict[str, bytes], aad: bytes = b"", context: bytes = b"") -> bytes:
|
|
127
|
+
key = derive_key(AEAD_LABEL, context)
|
|
128
|
+
alg = payload.get("alg", "AES-GCM")
|
|
129
|
+
nonce = payload.get("nonce")
|
|
130
|
+
ct = payload.get("ciphertext")
|
|
131
|
+
if not nonce or not ct:
|
|
132
|
+
raise ValueError("invalid payload for AEAD decrypt")
|
|
133
|
+
if alg.startswith("CHACHA20"):
|
|
134
|
+
aead = ChaCha20Poly1305(key)
|
|
135
|
+
return aead.decrypt(nonce, ct, aad)
|
|
136
|
+
else:
|
|
137
|
+
aead = AESGCM(key)
|
|
138
|
+
return aead.decrypt(nonce, ct, aad)
|
|
139
|
+
|
|
140
|
+
def compute_hmac(data: bytes, context: bytes = b"") -> bytes:
|
|
141
|
+
key = derive_key(HMAC_LABEL, context)
|
|
142
|
+
h = crypto_hmac.HMAC(key, hashes.SHA256())
|
|
143
|
+
h.update(data)
|
|
144
|
+
return h.finalize()
|
|
145
|
+
|
|
146
|
+
def verify_hmac(data: bytes, tag: bytes, context: bytes = b"") -> bool:
|
|
147
|
+
key = derive_key(HMAC_LABEL, context)
|
|
148
|
+
h = crypto_hmac.HMAC(key, hashes.SHA256())
|
|
149
|
+
h.update(data)
|
|
150
|
+
try:
|
|
151
|
+
h.verify(tag)
|
|
152
|
+
return True
|
|
153
|
+
except InvalidSignature:
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
def compute_hash(data: bytes) -> str:
|
|
157
|
+
if HASH_CHOICE == "SHA3":
|
|
158
|
+
h = hashes.Hash(hashes.SHA3_256())
|
|
159
|
+
else:
|
|
160
|
+
h = hashes.Hash(hashes.SHA256())
|
|
161
|
+
h.update(data)
|
|
162
|
+
return base64.b64encode(h.finalize()).decode()
|
|
163
|
+
|
|
164
|
+
# ----------------------------
|
|
165
|
+
# Funciones para tokens CSRF firmados/cifrados
|
|
166
|
+
# ----------------------------
|
|
167
|
+
def sign_csrf_token(token: str, context: bytes = b"") -> str:
|
|
168
|
+
"""Firma un token CSRF con HMAC-SHA256 para evitar alteraciones."""
|
|
169
|
+
data = token.encode("utf-8")
|
|
170
|
+
tag = compute_hmac(data, context)
|
|
171
|
+
return f"{token}.{base64.b64encode(tag).decode()}"
|
|
172
|
+
|
|
173
|
+
def verify_csrf_token_signature(signed_token: str, context: bytes = b"") -> str:
|
|
174
|
+
"""Verifica la firma de un token CSRF y retorna el token original si es válido."""
|
|
175
|
+
try:
|
|
176
|
+
token, tag_b64 = signed_token.rsplit(".", 1)
|
|
177
|
+
tag = base64.b64decode(tag_b64)
|
|
178
|
+
if verify_hmac(token.encode("utf-8"), tag, context):
|
|
179
|
+
return token
|
|
180
|
+
else:
|
|
181
|
+
raise ValueError("Invalid signature")
|
|
182
|
+
except Exception:
|
|
183
|
+
raise ValueError("Invalid signed CSRF token")
|
|
184
|
+
|
|
185
|
+
def encrypt_csrf_token(token: str, context: bytes = b"") -> str:
|
|
186
|
+
"""Cifra un token CSRF sensible con AEAD."""
|
|
187
|
+
enc = aead_encrypt(token.encode("utf-8"), context=context)
|
|
188
|
+
return base64.b64encode(json.dumps(enc).encode()).decode()
|
|
189
|
+
|
|
190
|
+
def decrypt_csrf_token(encrypted_token: str, context: bytes = b"") -> str:
|
|
191
|
+
"""Descifra un token CSRF."""
|
|
192
|
+
try:
|
|
193
|
+
enc = json.loads(base64.b64decode(encrypted_token))
|
|
194
|
+
plaintext = aead_decrypt(enc, context=context)
|
|
195
|
+
return plaintext.decode("utf-8")
|
|
196
|
+
except Exception:
|
|
197
|
+
raise ValueError("Invalid encrypted CSRF token")
|
|
198
|
+
|
|
199
|
+
# ----------------------------
|
|
200
|
+
# Registro cifrado de eventos (similar a XSS/SQLi)
|
|
201
|
+
# ----------------------------
|
|
202
|
+
def record_csrf_event(event: dict) -> None:
|
|
203
|
+
try:
|
|
204
|
+
ts = int(time.time())
|
|
205
|
+
# cifrar payload si existe
|
|
206
|
+
if "payload" in event and event["payload"]:
|
|
207
|
+
try:
|
|
208
|
+
ctx = f"{event.get('ip','')}-{ts}".encode()
|
|
209
|
+
enc = aead_encrypt(json.dumps(event["payload"], ensure_ascii=False).encode("utf-8"), context=ctx)
|
|
210
|
+
htag = compute_hmac(enc["ciphertext"], context=ctx)
|
|
211
|
+
event["_payload_encrypted"] = {
|
|
212
|
+
"alg": enc["alg"],
|
|
213
|
+
"nonce": base64.b64encode(enc["nonce"]).decode(),
|
|
214
|
+
"ciphertext": base64.b64encode(enc["ciphertext"]).decode(),
|
|
215
|
+
"hmac": base64.b64encode(htag).decode(),
|
|
216
|
+
}
|
|
217
|
+
del event["payload"] # no almacenar plaintext
|
|
218
|
+
except Exception:
|
|
219
|
+
# si falla, simplemente no incluimos payload
|
|
220
|
+
event.pop("payload", None)
|
|
221
|
+
key = f"csrf_event:{ts}:{event.get('ip', '')}"
|
|
222
|
+
cache.set(key, json.dumps(event, ensure_ascii=False), timeout=60 * 60 * 24)
|
|
223
|
+
except Exception:
|
|
224
|
+
logger.exception("record_csrf_event failed")
|
|
225
|
+
|
|
226
|
+
# ----------------------------
|
|
227
|
+
# Funciones auxiliares (igual que antes)
|
|
228
|
+
# ----------------------------
|
|
229
|
+
def get_client_ip(request):
|
|
230
|
+
x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR")
|
|
231
|
+
if x_forwarded_for:
|
|
232
|
+
ips = [ip.strip() for ip in x_forwarded_for.split(",") if ip.strip()]
|
|
233
|
+
if ips:
|
|
234
|
+
return ips[0]
|
|
235
|
+
return request.META.get("REMOTE_ADDR", "")
|
|
236
|
+
|
|
237
|
+
def host_from_header(header_value: str) -> str | None:
|
|
238
|
+
if not header_value:
|
|
239
|
+
return None
|
|
240
|
+
try:
|
|
241
|
+
parsed = urlparse(header_value)
|
|
242
|
+
if parsed.netloc:
|
|
243
|
+
return parsed.netloc.split(":")[0]
|
|
244
|
+
return header_value.split(":")[0]
|
|
245
|
+
except Exception:
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
def origin_matches_host(request) -> bool:
|
|
249
|
+
host_header = request.META.get("HTTP_HOST") or request.META.get("SERVER_NAME")
|
|
250
|
+
if not host_header:
|
|
251
|
+
return True
|
|
252
|
+
host = host_header.split(":")[0]
|
|
253
|
+
origin = request.META.get("HTTP_ORIGIN", "")
|
|
254
|
+
referer = request.META.get("HTTP_REFERER", "")
|
|
255
|
+
# Bloquear obvious javascript: referers
|
|
256
|
+
if any(re.search(r"(javascript:|<script|data:text/html)", h or "", re.I) for h in [origin, referer]):
|
|
257
|
+
return False
|
|
258
|
+
if origin_host := host_from_header(origin):
|
|
259
|
+
if origin_host == host:
|
|
260
|
+
return True
|
|
261
|
+
if referer_host := host_from_header(referer):
|
|
262
|
+
if referer_host == host:
|
|
263
|
+
return True
|
|
264
|
+
if not origin and not referer:
|
|
265
|
+
return True
|
|
266
|
+
return False
|
|
267
|
+
|
|
268
|
+
def has_csrf_token(request) -> bool:
|
|
269
|
+
for h in CSRF_HEADER_NAMES:
|
|
270
|
+
if request.META.get(h):
|
|
271
|
+
return True
|
|
272
|
+
if request.COOKIES.get(CSRF_COOKIE_NAME):
|
|
273
|
+
return True
|
|
274
|
+
try:
|
|
275
|
+
if request.method == "POST" and hasattr(request, "POST"):
|
|
276
|
+
if request.POST.get(POST_FIELD_NAME):
|
|
277
|
+
return True
|
|
278
|
+
except Exception:
|
|
279
|
+
pass
|
|
280
|
+
return False
|
|
281
|
+
|
|
282
|
+
def extract_payload_text(request) -> str:
|
|
283
|
+
parts: List[str] = []
|
|
284
|
+
try:
|
|
285
|
+
body = request.body.decode("utf-8", errors="ignore")
|
|
286
|
+
if body:
|
|
287
|
+
parts.append(body)
|
|
288
|
+
except Exception:
|
|
289
|
+
pass
|
|
290
|
+
qs = request.META.get("QUERY_STRING", "")
|
|
291
|
+
if qs:
|
|
292
|
+
parts.append(qs)
|
|
293
|
+
parts.append(request.META.get("HTTP_USER_AGENT", ""))
|
|
294
|
+
parts.append(request.META.get("HTTP_REFERER", ""))
|
|
295
|
+
return " ".join([p for p in parts if p])
|
|
296
|
+
|
|
297
|
+
def extract_parameters(request) -> List[str]:
|
|
298
|
+
params = []
|
|
299
|
+
if hasattr(request, "POST"):
|
|
300
|
+
params.extend(request.POST.keys())
|
|
301
|
+
if hasattr(request, "GET"):
|
|
302
|
+
params.extend(request.GET.keys())
|
|
303
|
+
try:
|
|
304
|
+
if request.body and "application/json" in (request.META.get("CONTENT_TYPE") or ""):
|
|
305
|
+
data = json.loads(request.body)
|
|
306
|
+
params.extend(data.keys())
|
|
307
|
+
except Exception:
|
|
308
|
+
pass
|
|
309
|
+
return params
|
|
310
|
+
|
|
311
|
+
# FUNCIÓN ROBUSTA: Analizar payload en TODOS los campos (sin descuento)
|
|
312
|
+
def analyze_payload(value: str) -> float:
|
|
313
|
+
score = 0.0
|
|
314
|
+
for patt, desc, weight in CSRF_PAYLOAD_PATTERNS:
|
|
315
|
+
if patt.search(value):
|
|
316
|
+
score += weight # Score full, sin descuento
|
|
317
|
+
return round(score, 3)
|
|
318
|
+
|
|
319
|
+
# NUEVA FUNCIÓN: Extraer y analizar query string
|
|
320
|
+
def analyze_query_string(request) -> float:
|
|
321
|
+
qs = request.META.get("QUERY_STRING", "")
|
|
322
|
+
if qs:
|
|
323
|
+
return analyze_payload(qs)
|
|
324
|
+
return 0.0
|
|
325
|
+
|
|
326
|
+
# NUEVA FUNCIÓN: Analizar headers adicionales
|
|
327
|
+
def analyze_headers(request) -> List[str]:
|
|
328
|
+
issues = []
|
|
329
|
+
ua = request.META.get("HTTP_USER_AGENT", "")
|
|
330
|
+
if re.search(r"(script|<|eval|bot|crawler)", ua, re.I):
|
|
331
|
+
issues.append("User-Agent sospechoso (posible automatización/bot)")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
accept_lang = request.META.get("HTTP_ACCEPT_LANGUAGE", "")
|
|
335
|
+
if not accept_lang or len(accept_lang) < 2:
|
|
336
|
+
issues.append("Accept-Language ausente o muy corto (posible bot)")
|
|
337
|
+
|
|
338
|
+
return issues
|
|
339
|
+
|
|
340
|
+
class CSRFDefenseMiddleware(MiddlewareMixin):
|
|
341
|
+
def process_request(self, request):
|
|
342
|
+
# Excluir APIs JSON si se configuró así
|
|
343
|
+
for prefix in CSRF_DEFENSE_EXCLUDED_API_PREFIXES:
|
|
344
|
+
if request.path.startswith(prefix):
|
|
345
|
+
logger.debug(f"[CSRFDefense] Skip analysis for API prefix {prefix} path {request.path}")
|
|
346
|
+
return None
|
|
347
|
+
|
|
348
|
+
client_ip = get_client_ip(request)
|
|
349
|
+
trusted_ips = getattr(settings, "CSRF_DEFENSE_TRUSTED_IPS", [])
|
|
350
|
+
if client_ip in trusted_ips:
|
|
351
|
+
return None
|
|
352
|
+
|
|
353
|
+
excluded_paths = getattr(settings, "CSRF_DEFENSE_EXCLUDED_PATHS", [])
|
|
354
|
+
if any(request.path.startswith(p) for p in excluded_paths):
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
method = (request.method or "").upper()
|
|
358
|
+
if method not in STATE_CHANGING_METHODS:
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
descripcion: List[str] = []
|
|
362
|
+
payload = extract_payload_text(request)
|
|
363
|
+
params = extract_parameters(request)
|
|
364
|
+
|
|
365
|
+
# 1) Falta token CSRF
|
|
366
|
+
if not has_csrf_token(request):
|
|
367
|
+
descripcion.append("Falta token CSRF en cookie/header/form")
|
|
368
|
+
|
|
369
|
+
# 2) Origin/Referer no coinciden
|
|
370
|
+
if not origin_matches_host(request):
|
|
371
|
+
descripcion.append("Origin/Referer no coinciden con Host (posible cross-site)")
|
|
372
|
+
|
|
373
|
+
# 3) Content-Type sospechoso
|
|
374
|
+
content_type = (request.META.get("CONTENT_TYPE") or "")
|
|
375
|
+
for patt in SUSPICIOUS_CT_PATTERNS:
|
|
376
|
+
if patt.search(content_type):
|
|
377
|
+
descripcion.append(f"Content-Type sospechoso: {content_type}")
|
|
378
|
+
break
|
|
379
|
+
|
|
380
|
+
# 4) Referer ausente y sin token CSRF
|
|
381
|
+
referer = request.META.get("HTTP_REFERER", "")
|
|
382
|
+
if not referer and not any(request.META.get(h) for h in CSRF_HEADER_NAMES):
|
|
383
|
+
descripcion.append("Referer ausente y sin X-CSRFToken")
|
|
384
|
+
|
|
385
|
+
# 5) Parámetros sensibles en GET/JSON
|
|
386
|
+
for p in params:
|
|
387
|
+
if p.lower() in SENSITIVE_PARAMS and method == "GET":
|
|
388
|
+
descripcion.append(f"Parámetro sensible '{p}' enviado en GET (posible CSRF)")
|
|
389
|
+
|
|
390
|
+
# 6) JSON sospechoso desde dominio externo
|
|
391
|
+
if "application/json" in content_type:
|
|
392
|
+
origin = request.META.get("HTTP_ORIGIN") or ""
|
|
393
|
+
if origin and host_from_header(origin) != (request.META.get("HTTP_HOST") or "").split(":")[0]:
|
|
394
|
+
descripcion.append("JSON POST desde origen externo (posible CSRF)")
|
|
395
|
+
|
|
396
|
+
# 7) Análisis ROBUSTO de payload en TODOS los campos (sin descuento)
|
|
397
|
+
payload_score = 0.0
|
|
398
|
+
payload_summary: List[Dict[str, Any]] = []
|
|
399
|
+
try:
|
|
400
|
+
# Analizar POST
|
|
401
|
+
if hasattr(request, "POST"):
|
|
402
|
+
for key, value in request.POST.items():
|
|
403
|
+
if isinstance(value, str):
|
|
404
|
+
score = analyze_payload(value)
|
|
405
|
+
payload_score += score
|
|
406
|
+
if score > 0:
|
|
407
|
+
payload_summary.append({"field": key, "snippet": value[:300], "score": score})
|
|
408
|
+
# Analizar JSON
|
|
409
|
+
if "application/json" in content_type:
|
|
410
|
+
data = json.loads(request.body.decode("utf-8") or "{}")
|
|
411
|
+
for key, value in data.items():
|
|
412
|
+
if isinstance(value, str):
|
|
413
|
+
score = analyze_payload(value)
|
|
414
|
+
payload_score += score
|
|
415
|
+
if score > 0:
|
|
416
|
+
payload_summary.append({"field": key, "snippet": value[:300], "score": score})
|
|
417
|
+
except Exception as e:
|
|
418
|
+
logger.debug(f"Error analizando payload: {e}")
|
|
419
|
+
|
|
420
|
+
if payload_score > 0:
|
|
421
|
+
descripcion.append(f"Payload sospechoso detectado (score total: {payload_score})")
|
|
422
|
+
|
|
423
|
+
# 8) Análisis de query string
|
|
424
|
+
qs_score = analyze_query_string(request)
|
|
425
|
+
if qs_score > 0:
|
|
426
|
+
descripcion.append(f"Query string sospechosa (score: {qs_score})")
|
|
427
|
+
payload_score += qs_score
|
|
428
|
+
|
|
429
|
+
# 9) Análisis de headers adicionales
|
|
430
|
+
header_issues = analyze_headers(request)
|
|
431
|
+
descripcion.extend(header_issues)
|
|
432
|
+
|
|
433
|
+
# Señales >= umbral => marcar y registrar evento cifrado
|
|
434
|
+
total_signals = len(descripcion)
|
|
435
|
+
if descripcion and total_signals >= CSRF_DEFENSE_MIN_SIGNALS:
|
|
436
|
+
w_csrf = getattr(settings, "CSRF_DEFENSE_WEIGHT", 0.2)
|
|
437
|
+
s_csrf = w_csrf * total_signals + payload_score # Score full sin descuento
|
|
438
|
+
request.csrf_attack_info = {
|
|
439
|
+
"ip": client_ip,
|
|
440
|
+
"tipos": ["CSRF"],
|
|
441
|
+
"descripcion": descripcion,
|
|
442
|
+
"payload": json.dumps(payload_summary, ensure_ascii=False)[:1000],
|
|
443
|
+
"score": s_csrf,
|
|
444
|
+
}
|
|
445
|
+
logger.warning(
|
|
446
|
+
"CSRF detectado desde IP %s: %s ; path=%s ; Content-Type=%s ; score=%.2f (Ultra-Robust: nada ignorado)",
|
|
447
|
+
client_ip, descripcion, request.path, content_type, s_csrf
|
|
448
|
+
)
|
|
449
|
+
# Registrar evento cifrado para auditoría
|
|
450
|
+
try:
|
|
451
|
+
record_csrf_event({
|
|
452
|
+
"ts": int(time.time()),
|
|
453
|
+
"ip": client_ip,
|
|
454
|
+
"score": s_csrf,
|
|
455
|
+
"desc": descripcion,
|
|
456
|
+
"url": request.build_absolute_uri(),
|
|
457
|
+
"payload": payload_summary, # se cifrará en record_csrf_event
|
|
458
|
+
})
|
|
459
|
+
except Exception:
|
|
460
|
+
logger.exception("failed to record CSRF event")
|
|
461
|
+
else:
|
|
462
|
+
if descripcion:
|
|
463
|
+
logger.debug(f"[CSRFDefense] low-signals ({total_signals}) not marking: {descripcion}")
|
|
464
|
+
|
|
465
|
+
return None
|
|
466
|
+
|
|
467
|
+
# =====================================================
|
|
468
|
+
# === INFORMACIÓN EXTRA ===
|
|
469
|
+
# =====================================================
|
|
470
|
+
"""
|
|
471
|
+
Algoritmos relacionados para protección contra CSRF:
|
|
472
|
+
- HMAC-SHA256: Usado para firmar tokens CSRF (sign_csrf_token, verify_csrf_token_signature).
|
|
473
|
+
- SHA-256/SHA-3: Usado para hashes de contenido (compute_hash).
|
|
474
|
+
- AES-GCM/ChaCha20-Poly1305: Usado para cifrar tokens sensibles o payloads (encrypt_csrf_token, decrypt_csrf_token).
|
|
475
|
+
- HKDF: Usado para derivar claves seguras (derive_key).
|
|
476
|
+
- Argon2id: Usado para derivar claves con resistencia a ataques de fuerza bruta (derive_key).
|
|
477
|
+
- Registro cifrado de eventos: Para asegurar auditoría sin exponer datos sensibles.
|
|
478
|
+
|
|
479
|
+
Contribución a fórmula de amenaza S:
|
|
480
|
+
S_csrf = w_csrf * señales_csrf + payload_score
|
|
481
|
+
Ejemplo: S_csrf = 0.2 * 3 + 1.5 = 2.1
|
|
482
|
+
|
|
483
|
+
Notas sobre implementación de algoritmos de seguridad:
|
|
484
|
+
- Integra con Django's CSRF middleware: Usa este middleware para detección adicional, y genera tokens firmados si es necesario.
|
|
485
|
+
- Para usar en producción: Configura CSRF_DEFENSE_MASTER_KEY en settings.py.
|
|
486
|
+
- Ajusta umbrales y configuraciones según necesidades.
|
|
487
|
+
- Combina con CSP (Content Security Policy) para mayor protección.
|
|
488
|
+
"""
|