Módulo 3: Detectar Hallucinations en Código
Hallucinations en Lógica
Hallucinations en Lógica
Descripción de la cápsula
Un import falso falla al importar. Un parámetro incorrecto falla (a veces) en runtime. Pero una hallucination en lógica — código que compila, ejecuta, produce un resultado, y ese resultado es incorrecto — es el tipo más peligroso porque puede llegar a producción sin que nadie lo note.
En la cápsula anterior trabajaste con imports y APIs: errores que herramientas automatizadas pueden atrapar. En esta cápsula entras al territorio donde tu ojo humano y tu conocimiento del dominio son la única defensa. No hay linter que detecte que una función calculate_percentile() implementa la fórmula incorrecta. No hay type checker que sepa que validate_email() debería hacer más que checar @.
Este es el nivel de detección que separa al developer que usa AI del developer que domina AI.
Qué Es una Hallucination de Lógica
Definición precisa
Una hallucination de lógica es código que:
- ✅ Compila sin errores
- ✅ Ejecuta sin excepciones
- ✅ Produce un resultado (no es void ni crashea)
- ❌ El resultado no corresponde a lo que el nombre de la función, el docstring, o la intención declarada prometen
La diferencia clave con un bug: un bug es un error de implementación donde el developer (humano o AI) intentó hacer algo y se equivocó. Una hallucination de lógica es una implementación que el LLM fabricó — nunca fue correcta, nunca se verificó contra una referencia.
Por qué son las más peligrosas
Import falso:
Tiempo de detección: inmediato (al importar)
Impacto: ninguno (no llega a producción)
Costo: 0
Parámetro incorrecto:
Tiempo de detección: minutos a horas
Impacto: bajo-medio (comportamiento inesperado)
Costo: horas de debugging
Lógica fabricada:
Tiempo de detección: días a semanas a NUNCA
Impacto: alto (resultados incorrectos en producción)
Costo: desde horas de debugging hasta daño al negocio
La lógica fabricada puede estar en producción durante semanas produciendo resultados incorrectos sin que nadie lo note — porque el código "funciona" y produce resultados que se ven razonables.
Los 6 Patrones de Lógica Fabricada
Patrón 1: Validación superficial
El LLM genera una función de validación que checa lo mínimo y lo presenta como validación completa.
import re
def validate_email(email: str) -> bool:
"""
Validates an email address according to RFC 5322.
Returns True if the email is valid, False otherwise.
"""
if not email or not isinstance(email, str):
return False
parts = email.split("@")
if len(parts) != 2:
return False
local, domain = parts
if not local or not domain:
return False
if "." not in domain:
return False
return True
Lo que dice hacer: Validar email según RFC 5322.
Lo que realmente hace: Checa que tenga @, algo antes, algo después, y un . en el dominio.
Lo que acepta y no debería:
validate_email("a@b.c") # True ← TLD de 1 letra
validate_email("us er@domain.com") # True ← Espacio en local part
validate_email("user@dom ain.com") # True ← Espacio en dominio
validate_email(".user@domain.com") # True ← Punto al inicio
validate_email("user@.domain.com") # True ← Punto después de @
validate_email("a" * 500 + "@b.com") # True ← Local part de 500 chars
La solución correcta:
from email_validator import validate_email as real_validate, EmailNotValidError
def validate_email(email: str) -> bool:
"""Validates an email address using the email-validator library."""
try:
real_validate(email, check_deliverability=False)
return True
except EmailNotValidError:
return False
Lección: Cuando la validación es compleja (email, URL, tarjeta de crédito, teléfono), desconfía de implementaciones manuales. Las librerías especializadas existen porque la validación correcta es difícil.
Patrón 2: Algoritmo incorrecto con nombre correcto
El LLM genera una función matemática o algorítmica con el nombre correcto pero la implementación es una aproximación incorrecta.
from typing import List
def calculate_median(data: List[float]) -> float:
"""
Calculates the median of a list of numbers.
For even-length lists, returns the average of the two middle values.
"""
if not data:
raise ValueError("Cannot calculate median of empty list")
sorted_data = sorted(data)
n = len(sorted_data)
mid = n // 2
if n % 2 == 0:
return (sorted_data[mid] + sorted_data[mid + 1]) / 2
else:
return sorted_data[mid]
El error: Para listas de longitud par, el código usa sorted_data[mid] y sorted_data[mid + 1], pero debería ser sorted_data[mid - 1] y sorted_data[mid].
data = [1, 2, 3, 4] # n=4, mid=2
# El código calcula: (sorted_data[2] + sorted_data[3]) / 2 = (3 + 4) / 2 = 3.5
# Lo correcto: (sorted_data[1] + sorted_data[2]) / 2 = (2 + 3) / 2 = 2.5
Por qué es sutil: Para listas de longitud impar, funciona perfectamente. Para listas de longitud par, el error es de un índice — el resultado es cercano pero no correcto. Con datasets grandes, la diferencia puede ser tan pequeña que nadie la nota.
Cómo verificar:
import statistics
def test_median():
assert calculate_median([1, 2, 3]) == statistics.median([1, 2, 3]) # OK
assert calculate_median([1, 2, 3, 4]) == statistics.median([1, 2, 3, 4]) # FAIL
assert calculate_median([1]) == statistics.median([1]) # OK
Patrón 3: Seguridad que da falsa confianza
El LLM genera código de seguridad que parece robusto pero tiene vulnerabilidades fundamentales.
import hashlib
import os
def hash_password(password: str) -> str:
"""
Securely hashes a password using SHA-256 with a random salt.
Returns the salt and hash concatenated.
"""
salt = os.urandom(16).hex()
salted_password = salt + password
password_hash = hashlib.sha256(salted_password.encode()).hexdigest()
return f"{salt}:{password_hash}"
def verify_password(password: str, stored_hash: str) -> bool:
"""Verifies a password against a stored hash."""
salt, password_hash = stored_hash.split(":")
salted_password = salt + password
return hashlib.sha256(salted_password.encode()).hexdigest() == password_hash
Lo que parece: Hashing seguro con salt random. Lo que realmente es: SHA-256 es un hash de propósito general, no un hash para passwords.
Los problemas:
- SHA-256 es demasiado rápido — permite ~10 mil millones de hashes/segundo en GPU moderna
- No tiene key stretching (bcrypt hace 2^12 iteraciones por defecto)
- No tiene protección contra timing attacks en la comparación (
==vshmac.compare_digest) - Un atacante con acceso a la base de datos puede brute-force passwords eficientemente
La solución correcta:
from passlib.hash import bcrypt
def hash_password(password: str) -> str:
"""Securely hashes a password using bcrypt."""
return bcrypt.hash(password)
def verify_password(password: str, stored_hash: str) -> bool:
"""Verifies a password against its bcrypt hash."""
return bcrypt.verify(password, stored_hash)
Lección: Para funciones de seguridad (hashing, encryption, token generation, sanitización), siempre usa librerías especializadas. Nunca confíes en implementaciones manuales, incluso si parecen razonables.
Patrón 4: Sanitización incompleta
El LLM genera una función de sanitización que cubre los casos obvios pero deja vectores de ataque abiertos.
import re
def sanitize_html(text: str) -> str:
"""
Sanitizes HTML input to prevent XSS attacks.
Removes all script tags and event handlers.
"""
sanitized = re.sub(r"<script[^>]*>.*?</script>", "", text, flags=re.DOTALL | re.IGNORECASE)
sanitized = re.sub(r'\s+on\w+\s*=\s*"[^"]*"', "", sanitized, flags=re.IGNORECASE)
sanitized = re.sub(r"\s+on\w+\s*=\s*'[^']*'", "", sanitized, flags=re.IGNORECASE)
sanitized = re.sub(r"javascript:", "", sanitized, flags=re.IGNORECASE)
return sanitized
Lo que remueve:
<script>alert('xss')</script> <!-- ✅ Removido -->
<div onclick="alert('xss')">Click</div> <!-- ✅ Removido -->
<a href="javascript:alert('xss')">Link</a> <!-- ✅ Removido -->
Lo que NO remueve:
<img src=x onerror=alert('xss')> <!-- ❌ Sin comillas en atributo -->
<svg onload=alert('xss')> <!-- ❌ Sin comillas en atributo -->
<div style="background:url(javascript:alert(1))"> <!-- ❌ CSS injection -->
<iframe src="data:text/html,<script>alert(1)</script>"> <!-- ❌ data: URI -->
<a href="javascript:alert(1)">Link</a> <!-- ❌ HTML entities -->
La solución correcta:
import bleach
def sanitize_html(text: str) -> str:
"""Sanitizes HTML using bleach library."""
return bleach.clean(
text,
tags=["p", "br", "b", "i", "u", "a", "ul", "ol", "li"],
attributes={"a": ["href"]},
protocols=["https"],
strip=True,
)
Patrón 5: Conversión con pérdida de precisión
El LLM genera código de conversión que pierde precisión de maneras no obvias.
from datetime import datetime, timezone
def unix_timestamp_to_datetime(timestamp: float) -> datetime:
"""
Converts a Unix timestamp to a timezone-aware datetime object.
Handles both seconds and milliseconds timestamps.
"""
if timestamp > 1e12:
timestamp = timestamp / 1000
return datetime.fromtimestamp(timestamp, tz=timezone.utc)
def calculate_duration_hours(start: datetime, end: datetime) -> float:
"""
Calculates the duration between two datetimes in hours.
Returns a float with the fractional hours.
"""
delta = end - start
return delta.seconds / 3600
El error en calculate_duration_hours: Usa delta.seconds en lugar de delta.total_seconds().
from datetime import timedelta
delta = timedelta(days=1, hours=2, minutes=30)
delta.seconds # → 9000 (solo la parte de seconds, ignora days!)
delta.total_seconds() # → 95400.0 (total correcto incluyendo days)
delta.seconds solo retorna los segundos dentro del día actual, ignorando los días completos. Si start y end difieren por más de 24 horas, el resultado es completamente incorrecto.
La corrección:
def calculate_duration_hours(start: datetime, end: datetime) -> float:
"""Calculates the duration between two datetimes in hours."""
delta = end - start
return delta.total_seconds() / 3600
Lección: Los métodos de timedelta son confusos. delta.seconds no es "el total de segundos" — es un detalle de la API de Python que los LLMs y muchos developers confunden.
Patrón 6: Concurrencia incorrecta
El LLM genera código async que parece correcto pero tiene race conditions o deadlocks potenciales.
import asyncio
from typing import Dict, Any
class AsyncCache:
"""Thread-safe async cache with TTL support."""
def __init__(self):
self._cache: Dict[str, Any] = {}
self._lock = asyncio.Lock()
async def get(self, key: str) -> Any:
"""Gets a value from cache. Returns None if not found."""
return self._cache.get(key)
async def set(self, key: str, value: Any) -> None:
"""Sets a value in the cache."""
async with self._lock:
self._cache[key] = value
async def get_or_set(self, key: str, factory) -> Any:
"""Gets a value from cache, or creates it using factory if not found."""
value = await self.get(key)
if value is None:
value = await factory()
await self.set(key, value)
return value
Los errores:
-
get()no usa el lock: Si otro task está modificando_cachemientrasget()lee, puede haber inconsistencias. Debería usar el lock también. -
get_or_set()tiene una race condition: Entreget()yset(), otro task puede haber hechoset()con el mismo key. El factory se ejecuta múltiples veces innecesariamente. El patrón correcto es check-lock-check (double-checked locking).
La corrección:
async def get(self, key: str) -> Any:
async with self._lock:
return self._cache.get(key)
async def get_or_set(self, key: str, factory) -> Any:
async with self._lock:
value = self._cache.get(key)
if value is None:
value = await factory()
self._cache[key] = value
return value
Cómo Detectar Hallucinations de Lógica
El principio fundamental: desconfía de las implementaciones manuales
El patrón que conecta todos los ejemplos anteriores:
Email validation → Librería: email-validator
Password hashing → Librería: passlib / bcrypt
HTML sanitization → Librería: bleach
SQL sanitization → Parameterized queries (no sanitización)
Cálculos estadísticos → Librería: statistics / numpy
Cálculos de tiempo → Métodos de datetime (total_seconds, no seconds)
Concurrencia → Patrones establecidos con locks completos
Regla: Cuando un LLM implementa algo desde cero que una librería ya hace, sospecha. Las librerías existen porque la implementación correcta es difícil. Si el LLM evitó la librería e implementó manualmente, pregúntate por qué — y luego verifica la implementación.
Las 5 preguntas de detección
Cuando revisas una función generada por AI, hazte estas preguntas:
1. ¿Existe una librería estándar para esto?
Si sí → ¿por qué el código no la usa?
2. ¿El docstring promete más de lo que la implementación hace?
Si sí → La implementación probablemente es incompleta
3. ¿La función maneja edge cases?
Prueba: None, vacío, negativo, muy grande, caracteres especiales
4. ¿El resultado es verificable contra una referencia?
Para cálculos: usa numpy/statistics como referencia
Para validación: usa la librería estándar como referencia
5. ¿La función toca seguridad?
Si sí → NUNCA confíes en implementaciones manuales
Técnica: el quick test de 3 minutos
Para cualquier función sospechosa, escribe 3 tests en 3 minutos:
def verify_function(func, test_cases):
"""Quick verification of a function against test cases."""
for inputs, expected in test_cases:
result = func(*inputs) if isinstance(inputs, tuple) else func(inputs)
status = "✅" if result == expected else "❌"
print(f"{status} func({inputs}) = {result}, expected {expected}")
# Para validate_email:
verify_function(validate_email, [
("user@domain.com", True), # caso normal
("@domain.com", False), # sin local part
("user@.com", False), # dominio inválido
("us er@domain.com", False), # espacio
("", False), # vacío
])
# Para calculate_median:
import statistics
data_sets = [[1,2,3], [1,2,3,4], [5], [1,1,1,1]]
for data in data_sets:
expected = statistics.median(data)
result = calculate_median(data)
status = "✅" if result == expected else "❌"
print(f"{status} median({data}) = {result}, expected {expected}")
Si algún test falla, la función tiene una hallucination de lógica. Si todos pasan, no significa que sea correcta — pero los edge cases más comunes están cubiertos.
Dominios de Alto Riesgo
Dónde buscar primero
No puedes revisar cada función en detalle. Prioriza la revisión de lógica en estos dominios:
Prioridad Crítica:
- ✅ Autenticación y autorización (login, tokens, permisos)
- ✅ Hashing y encryption (passwords, secrets, PII)
- ✅ Sanitización de input (SQL, HTML, command injection)
- ✅ Validación de datos financieros (cálculos, conversiones)
Prioridad Alta:
- ✅ Validación de datos (email, teléfono, tarjeta de crédito)
- ✅ Cálculos matemáticos/estadísticos (percentiles, promedios, aggregations)
- ✅ Manejo de fechas y tiempos (zonas horarias, duraciones, conversiones)
- ✅ Concurrencia y operaciones async (locks, race conditions)
Prioridad Media:
- ⚠️ Formateo y serialización de datos
- ⚠️ Paginación y filtrado
- ⚠️ Transformaciones de datos
Prioridad Baja:
- ❌ Boilerplate CRUD (generalmente correcto)
- ❌ Configuración y setup
- ❌ Imports y definiciones de modelos
La regla de seguridad: nunca implementaciones manuales
Para funciones de seguridad, la regla es absoluta:
NUNCA aceptar implementaciones manuales de:
├── Password hashing → usar bcrypt / argon2
├── Token generation → usar secrets module
├── Encryption → usar cryptography library
├── SQL queries → usar parameterized queries
├── HTML sanitization → usar bleach
├── CSRF tokens → usar framework (FastAPI/Django)
├── Session management → usar framework
└── Input validation → usar librerías especializadas
Si Claude Code genera una implementación manual de
cualquiera de estos → RECHAZAR automáticamente.
No importa si el código "se ve bien."
Hallucinations de Lógica en FastAPI
Ejemplos específicos del framework
Como el proyecto integrador usa FastAPI, estos son los patrones de hallucination de lógica más comunes en este framework:
Paginación con off-by-one:
@app.get("/items")
async def list_items(page: int = 1, size: int = 20):
"""Returns paginated items."""
# ❌ Hallucination: off-by-one en el cálculo
start = page * size
end = start + size
items = all_items[start:end]
# Para page=1, size=20: devuelve items[20:40] — se salta los primeros 20
# ✅ Correcto:
start = (page - 1) * size
end = start + size
items = all_items[start:end]
# Para page=1, size=20: devuelve items[0:20] — correcto
return {"items": items, "total": len(all_items), "page": page}
Filtro invertido:
@app.get("/tasks")
async def get_active_tasks(include_completed: bool = False):
"""Returns active tasks. Optionally includes completed ones."""
tasks = get_all_tasks()
if include_completed:
# ❌ Hallucination: filtra lo opuesto
return [t for t in tasks if t.status != "completed"]
# ✅ Correcto:
if not include_completed:
return [t for t in tasks if t.status != "completed"]
return tasks
Dependency injection con scope incorrecto:
from fastapi import FastAPI, Depends
app = FastAPI()
# ❌ Hallucination: la conexión se crea una vez y se reusa
# En lugar de crearse por request
db_connection = create_db_connection()
async def get_db():
"""Provides a database connection."""
return db_connection # ← Mismo objeto para todos los requests
# ✅ Correcto: crear y cerrar conexión por request
async def get_db():
"""Provides a database session per request."""
db = create_db_session()
try:
yield db
finally:
db.close()
Conexión con Proyecto
Qué buscar en el proyecto integrador
En el codebase del proyecto integrador (módulo 8), hay al menos 1 hallucination de lógica plantada. El tipo de hallucination que podrías encontrar:
- Una función de validación que no valida correctamente
- Un cálculo que usa el operador o método incorrecto
- Un filtro que incluye en vez de excluir (o viceversa)
- Una función de seguridad que no es realmente segura
Tu proceso para encontrarla:
- Identifica funciones con nombres que prometen algo específico (validate, calculate, sanitize, hash)
- Lee la implementación — ¿hace realmente lo que promete?
- Escribe quick tests con edge cases
- Si existe librería estándar para esa función, compara contra ella
Del análisis a la acción
Cuando encuentres una hallucination de lógica en el proyecto:
- Documenta: qué dice hacer vs qué realmente hace
- Clasifica: severidad del impacto (Critical si es seguridad, High si es lógica de negocio)
- Corrige: idealmente usando la librería estándar, no parcheando la implementación manual
Troubleshooting
Problema 1: "No puedo distinguir entre una hallucination de lógica y un bug"
Causa: La distinción es sutil y a veces académica. Lo importante es detectar el error, no clasificarlo perfectamente.
Solución: Si el nombre/docstring dice una cosa y la implementación hace otra, trátalo como hallucination. Si la implementación intenta hacer lo correcto pero tiene un bug, trátalo como bug. En ambos casos, la acción es la misma: corregir. La clasificación importa para entender el patrón (los LLMs tienden a ciertos tipos de hallucinations) pero no cambia la acción.
Problema 2: "No tengo suficiente conocimiento del dominio para saber si la implementación es correcta"
Causa: No puedes ser experto en todos los dominios. Es normal.
Solución: Usa estas estrategias:
- Busca una librería: Si existe una librería para eso, compara contra ella
- Busca en la documentación oficial: Para funciones de Python estándar, la documentación tiene ejemplos
- Quick test con valores conocidos: Para cálculos, usa una calculadora o Wolfram Alpha
- Pregunta a Claude Code: "Is this implementation of X correct?" — pero verifica la respuesta con tests
Problema 3: "El quick test pasa pero no estoy seguro de que la función sea correcta"
Causa: Tus tests cubren los happy paths pero no los edge cases.
Solución: Agrega estos edge cases a tu quick test:
edge_cases = [
None, # null
"", # string vacío
[], # lista vacía
0, # cero
-1, # negativo
float('inf'), # infinito
float('nan'), # NaN
" ", # solo espacios
"a" * 10000, # string muy largo
[1], # lista de un elemento
[1, 1, 1, 1], # todos iguales
]
Si la función pasa todos los edge cases relevantes para su dominio, puedes tener mayor confianza.
Problema 4: "Revisar la lógica de cada función toma demasiado tiempo"
Causa: No necesitas revisar cada función. Prioriza por dominio de riesgo.
Solución: Sigue la priorización de la sección "Dominios de Alto Riesgo". En un code review típico:
- Funciones de seguridad: siempre revisa (5-10 minutos por función)
- Funciones de validación/cálculo: revisa si no usan librería estándar (3-5 minutos)
- Funciones CRUD: revisa rápidamente la lógica (1-2 minutos)
- Boilerplate: skip (0 minutos)
Problema 5: "Claude Code generó una implementación manual de algo que tiene librería. ¿Siempre es malo?"
Causa: A veces hay razones válidas para no usar una librería (minimizar dependencias, requerimientos específicos).
Solución: Si hay una razón válida para la implementación manual, verifica exhaustivamente. Si no hay razón clara, reemplaza con la librería. La regla: una implementación manual necesita justificación. Si no hay justificación, es probablemente una hallucination donde el LLM no "recordó" que la librería existe.
Ejercicios
Ejercicio 1: Detectar validación superficial (Fácil)
Esta función dice validar URLs. ¿Es correcta?
from urllib.parse import urlparse
def validate_url(url: str) -> bool:
"""
Validates that a URL is properly formatted and uses
a safe protocol (http or https).
"""
try:
result = urlparse(url)
return result.scheme in ("http", "https") and bool(result.netloc)
except Exception:
return False
Ver solución
Esta implementación es razonablemente correcta para una validación básica. A diferencia de validate_email() que solo checa @, esta función:
- ✅ Usa
urlparsede la librería estándar (no implementa parsing manual) - ✅ Verifica que el scheme sea http o https
- ✅ Verifica que haya un netloc (dominio)
- ✅ Maneja excepciones
Sin embargo, tiene limitaciones:
validate_url("http://localhost") # True — ¿es esto deseado?
validate_url("http://192.168.1.1") # True — ¿IP privada es válida?
validate_url("https://example..com") # True — doble punto en dominio
Veredicto: No es una hallucination — es una implementación correcta para el caso general. Las limitaciones son edge cases que dependen del contexto de uso. Para la mayoría de aplicaciones, esta validación es suficiente.
Lección: No todo lo que AI genera es incorrecto. Saber cuándo el código es "suficientemente bueno" también es una habilidad importante.
Ejercicio 2: Encontrar el error de cálculo (Medio)
Esta función calcula un descuento progresivo. ¿El cálculo es correcto?
def calculate_discount(subtotal: float, coupon_percent: float = 0) -> dict:
"""
Calculates the final price with progressive discount:
- Orders >= $100: 5% discount
- Orders >= $500: 10% discount
- Orders >= $1000: 15% discount
Plus any coupon discount applied AFTER the progressive discount.
"""
if subtotal < 100:
progressive_discount = 0
elif subtotal < 500:
progressive_discount = 5
elif subtotal < 1000:
progressive_discount = 10
else:
progressive_discount = 15
after_progressive = subtotal * (1 - progressive_discount / 100)
coupon_discount_amount = subtotal * (coupon_percent / 100)
final_price = after_progressive - coupon_discount_amount
return {
"subtotal": subtotal,
"progressive_discount_percent": progressive_discount,
"coupon_discount_percent": coupon_percent,
"final_price": max(final_price, 0),
}
Ver solución
Hay una hallucination de lógica en cómo se aplica el cupón:
El docstring dice: "coupon discount applied AFTER the progressive discount." Pero la implementación calcula el cupón sobre el subtotal original, no sobre el precio después del descuento progresivo.
# Lo que dice hacer (cupón sobre precio con descuento):
after_progressive = 1000 * (1 - 15/100) = 850
coupon_10 = 850 * (10/100) = 85
final = 850 - 85 = 765
# Lo que realmente hace (cupón sobre subtotal original):
after_progressive = 1000 * (1 - 15/100) = 850
coupon_10 = 1000 * (10/100) = 100 # ← Sobre subtotal original!
final = 850 - 100 = 750 # ← $15 menos de lo correcto
La corrección:
coupon_discount_amount = after_progressive * (coupon_percent / 100)
Impacto: Para un negocio, esto significa dar más descuento del que se pretende. Con miles de transacciones, la diferencia se acumula.
Ejercicio 3: Detectar seguridad falsa (Medio)
¿Este código de generación de tokens es seguro?
import random
import string
import time
def generate_reset_token(user_id: str) -> str:
"""
Generates a secure password reset token.
Token is unique per user and time-based for expiration.
"""
timestamp = str(int(time.time()))
random_part = ''.join(
random.choices(string.ascii_letters + string.digits, k=32)
)
token = f"{user_id}_{timestamp}_{random_part}"
return token
def verify_reset_token(token: str, max_age_seconds: int = 3600) -> str:
"""
Verifies a password reset token and returns the user_id.
Returns None if token is expired.
"""
parts = token.split("_")
if len(parts) != 3:
return None
user_id, timestamp, random_part = parts
token_age = int(time.time()) - int(timestamp)
if token_age > max_age_seconds:
return None
return user_id
Ver solución
Este código tiene múltiples hallucinations de lógica de seguridad:
random.choicesno es criptográficamente seguro. El módulorandomusa Mersenne Twister, que es predecible si se conoce el estado. Para tokens de seguridad, se debe usarsecrets:
import secrets
random_part = secrets.token_urlsafe(32)
-
El user_id está en el token en texto plano. Un atacante puede ver el user_id y generar tokens para cualquier usuario si conoce el patrón.
-
No hay firma ni verificación de integridad. Cualquiera que conozca el formato puede fabricar un token válido. Solo necesita: un user_id válido, un timestamp reciente, y 32 caracteres random. La función
verify_reset_tokenno verifica que el token fue generado por el sistema — solo verifica el formato y la expiración. -
El token se puede manipular. Un atacante puede tomar el user_id de un token robado y cambiar el timestamp para "renovar" el token.
La solución correcta:
import secrets
import hmac
import hashlib
import time
SECRET_KEY = "your-secret-key-from-env"
def generate_reset_token(user_id: str) -> str:
timestamp = str(int(time.time()))
random_part = secrets.token_urlsafe(32)
payload = f"{user_id}.{timestamp}.{random_part}"
signature = hmac.new(
SECRET_KEY.encode(), payload.encode(), hashlib.sha256
).hexdigest()
return f"{payload}.{signature}"
# O mejor aún, usar itsdangerous o PyJWT:
from itsdangerous import URLSafeTimedSerializer
serializer = URLSafeTimedSerializer(SECRET_KEY)
token = serializer.dumps(user_id, salt="password-reset")
Ejercicio 4: Verificar con quick test (Medio-Difícil)
Escribe 5 test cases que expondrían la hallucination en esta función:
def is_palindrome(text: str) -> bool:
"""
Checks if a string is a palindrome.
Ignores case and non-alphanumeric characters.
"""
cleaned = ''.join(c.lower() for c in text if c.isalnum())
return cleaned == cleaned[::-1]
Ver solución
Sorpresa: esta implementación es correcta. Los 5 test cases lo confirman:
assert is_palindrome("racecar") == True # ✅ Palindrome simple
assert is_palindrome("A man, a plan, a canal: Panama") == True # ✅ Con puntuación
assert is_palindrome("hello") == False # ✅ No es palindrome
assert is_palindrome("") == True # ✅ String vacío (convención)
assert is_palindrome("Aa") == True # ✅ Case insensitive
El punto de este ejercicio: El quick test no solo detecta hallucinations — también te confirma cuándo el código es correcto. Saber detenerte y aceptar que el código es bueno es tan importante como detectar errores. Si pasas 20 minutos buscando un error que no existe, es tiempo perdido.
Ejercicio 5: Analizar lógica completa (Difícil)
Este código implementa rate limiting. Encuentra todas las hallucinations de lógica:
import time
from collections import defaultdict
from typing import Optional
class RateLimiter:
"""
Token bucket rate limiter.
Allows 'rate' requests per 'period' seconds.
"""
def __init__(self, rate: int = 10, period: float = 60.0):
self.rate = rate
self.period = period
self.tokens = defaultdict(lambda: rate)
self.last_update = defaultdict(float)
def is_allowed(self, client_id: str) -> bool:
"""
Check if a request is allowed for the given client.
Uses token bucket algorithm.
"""
now = time.time()
elapsed = now - self.last_update[client_id]
self.tokens[client_id] += elapsed * (self.rate / self.period)
if self.tokens[client_id] > self.rate:
self.tokens[client_id] = self.rate
self.last_update[client_id] = now
if self.tokens[client_id] >= 1:
self.tokens[client_id] -= 1
return True
return False
def get_wait_time(self, client_id: str) -> Optional[float]:
"""Returns seconds to wait before next allowed request."""
if self.tokens[client_id] >= 1:
return 0.0
tokens_needed = 1 - self.tokens[client_id]
return tokens_needed / (self.rate / self.period)
Ver solución
Este es un caso interesante: la implementación del token bucket es fundamentalmente correcta. El algoritmo:
- ✅ Calcula tokens ganados desde la última actualización
- ✅ Limita tokens al máximo (rate)
- ✅ Consume un token si disponible
- ✅
get_wait_timecalcula el tiempo de espera correcto
Sin embargo, hay issues prácticos (no hallucinations de lógica, sino de diseño):
-
⚠️ No es thread-safe. Si múltiples requests del mismo client llegan simultáneamente, hay race condition entre leer y escribir tokens. Necesita un lock.
-
⚠️ Memory leak.
tokensylast_updatecrecen indefinidamente — nunca se limpian entries de clients inactivos. -
⚠️ No es distribuido. En un sistema con múltiples workers/pods, cada instancia tiene su propio rate limiter. Debería usar Redis para estado compartido.
Veredicto: La lógica del algoritmo es correcta. Los issues son de diseño para producción, no hallucinations. Este es un buen ejemplo de código que "funciona" pero necesita refinamiento para ser production-ready.
Lección: No todo issue es una hallucination. Distinguir entre "la lógica está mal" (hallucination) y "el diseño no es production-ready" (mejora de diseño) es importante para priorizar tu revisión.
Resumen
En esta cápsula aprendiste:
- Las hallucinations de lógica son las más peligrosas porque el código compila, ejecuta, y produce resultados — solo que incorrectos
- Los 6 patrones principales: validación superficial, algoritmo incorrecto, seguridad falsa, sanitización incompleta, pérdida de precisión, concurrencia incorrecta
- La regla de implementaciones manuales: si existe una librería para eso, desconfía del código manual
- Las 5 preguntas de detección: librería estándar, docstring vs implementación, edge cases, referencia verificable, toca seguridad
- El quick test de 3 minutos: 5 test cases con edge cases pueden exponer la mayoría de hallucinations
- Los dominios de alto riesgo: seguridad, finanzas, validación, cálculos, fechas, concurrencia
- No todo es hallucination: saber cuándo el código es correcto es igual de importante que detectar errores
Próxima cápsula: Herramientas de Detección — type checkers, linters, tests rápidos, y documentación oficial como red de seguridad.
Recursos Adicionales
- OWASP — Password Storage Cheat Sheet - Por qué SHA-256 no es para passwords
- OWASP — Input Validation Cheat Sheet - Validación correcta vs sanitización incorrecta
- Python secrets module - Generación criptográficamente segura de tokens
- email-validator library - Validación correcta de email en Python
- bleach library - Sanitización correcta de HTML
- Real Python — Common Python Gotchas - timedelta.seconds vs total_seconds y otros gotchas
Debugging & Code Review with Claude Code — Módulo 3, Cápsula 04 Claude Code Agentic Development Path — Guía #6 de 11