Deja de Usar console.log() Así: Es una Fuga de Datos Esperando Pasar

Deja de Usar console.log() Así: Es una Fuga de Datos Esperando Pasar

Cada día, desarrolladores exponen tokens, passwords y datos personales sin darse cuenta. Este artículo explica los riesgos reales y las alternativas seguras.


El Problema con console.log()

console.log() es la herramienta de debugging más usada. Es rápida, no requiere imports, y funciona en cualquier ambiente JavaScript.

console.log("User data:", user);

Pero esa línea “inocente” puede haber filtrado el email, JWT token, session cookie, o datos de tarjeta de crédito del usuario a lugares que no controlas: el browser console, logs de cloud, o sistemas de terceros.

Esto no es un riesgo teórico. Muchos incidentes de seguridad — desde fugas internas hasta violaciones de compliance — comenzaron con algo tan simple como console.log(user).

Los desarrolladores ven console.log como “solo debugging”, pero la realidad es más complicada. En pipelines modernos de CI/CD y cloud, los logs persisten, se replican, y a veces se comparten con sistemas externos. Lo que empezó como “debugging temporal” puede convertirse en exposición permanente.


¿Dónde Terminan Tus Logs?

Un console.log() puede terminar en múltiples lugares, cada uno con sus propios riesgos:

1. Browser Console

En código frontend, cualquier usuario con acceso a DevTools (F12) puede ver lo que logueas:

  • Tokens de autenticación
  • Respuestas de API
  • Variables de configuración
  • Datos de sesión

Además, muchas herramientas de analytics y bug tracking scrapean errores de consola automáticamente, enviando tus logs a servicios externos.

2. Server Logs

En Node.js, el output de console.log() termina en:

  • CloudWatch (AWS)
  • Stackdriver (GCP)
  • Azure Monitor
  • Docker container logs
  • Artifacts de CI/CD

Estos logs pueden vivir semanas o meses, indexados y respaldados — accesibles para personas fuera de tu perímetro de seguridad (contratistas, SREs, equipos de ops externos).

console.log("User login:", req.body);

Esa línea puede haber creado una copia permanente de un password en cloud storage.

3. Log Aggregation

En arquitecturas de microservicios, los logs fluyen por múltiples capas — a veces sin encriptar — antes de llegar a ELK Stack, Datadog, o Splunk.

Cada hop es un punto de exposición potencial. Y una vez que los datos llegan a un agregador, es extremadamente difícil eliminarlos selectivamente.


Por Qué console.log() Es Fundamentalmente Inseguro

1. Sin Clasificación de Datos

console.log no distingue entre datos sensibles y no sensibles. Logueas objetos completos, que frecuentemente contienen datos anidados con secretos:

console.log(user);
// Output real:
{
  id: "u1234",
  email: "alice@company.com",
  token: "eyJhbGciOi...",       // JWT expuesto
  password: "hashedButStillBad" // Hash visible
}

2. Sin Control de Acceso

Una vez impreso, el log está abierto para cualquiera que pueda leerlo — devs, ops, usuarios (en browser). No hay modelo de permisos ni sistema de redacción.

3. Sin Gestión de Ciclo de Vida

Los logs son “para siempre”. Incluso logs de debugging “temporales” persisten en artifacts de CI/CD, containers Docker, o logs serverless.

Sin políticas de retención o sanitización, no puedes garantizar eliminación de datos — un problema directo de compliance bajo GDPR, SOC 2, y HIPAA.


Cómo Atacantes Explotan Logs

Los atacantes aman los logs porque los logs dicen la verdad. Si tu código loguea demasiado, esencialmente se auto-documenta para quien gane acceso.

Token Harvesting

Si los logs contienen JWTs o API tokens, un atacante con acceso a logs puede autenticarse como usuarios reales. Incluso tokens expirados revelan estructura interna (user IDs, algoritmos de firma).

Error Message Leaks

Errores mal manejados pueden exponer stack traces, paths internos, o queries SQL:

console.error("DB Error:", error);
// Si error contiene el query raw, acabas de regalar detalles del schema

Movimiento Lateral

En sistemas complejos, microservicios internos comparten logs. Un servicio comprometido puede contener credenciales o endpoints internos en sus logs, dando puntos de pivot para moverse lateralmente.

Ingeniería Social

Los logs a veces capturan emails internos, IPs, o variables de entorno. Los atacantes pueden usar esto como base para phishing o credential stuffing.


Logging Seguro: Los Principios

Logging seguro no es evitar logs — es controlar qué se loguea, cómo, y dónde.

1. Sanitización y Masking

Antes de escribir cualquier log, sanitiza. Enmascara campos sensibles como passwords, tokens, o PII:

function sanitize(obj) {
  const clone = { ...obj };
  const sensitiveFields = ['password', 'token', 'apiKey', 'secret', 'ssn', 'creditCard'];
  
  for (const field of sensitiveFields) {
    if (clone[field]) clone[field] = '[REDACTED]';
  }
  
  // Sanitizar campos anidados comunes
  if (clone.user?.password) clone.user.password = '[REDACTED]';
  if (clone.headers?.authorization) clone.headers.authorization = '[REDACTED]';
  
  return clone;
}

// Uso
console.log("Request:", sanitize(req.body));

2. Log Levels

Usa niveles estandarizados (debug, info, warn, error) para separar ambientes:

Level Ambiente Contenido
debug Solo local dev Datos completos para debugging
info Todos Logs operacionales no-sensibles
warn Todos Situaciones anómalas
error Todos Excepciones sanitizadas

Un framework de logging puede automáticamente suprimir debug en producción.

3. Logging Estructurado

En lugar de texto libre, usa JSON estructurado:

// ❌ Malo: texto libre
console.log("User " + userId + " logged in from " + ip);

// ✅ Bueno: JSON estructurado
logger.info({
  event: 'user_login',
  userId: userId,
  ip: maskIP(ip),
  timestamp: new Date().toISOString()
});

Esto hace los logs machine-readable y más fáciles de filtrar/sanitizar.

4. Control de Acceso Centralizado

Almacena logs en sistemas controlados:

  • Elasticsearch + Kibana (con restricciones de acceso)
  • Datadog, Splunk, Loki
  • Cloud-native (AWS CloudWatch, GCP Logging)

Asegura:

  • HTTPS/TLS en tránsito
  • Role-based access control
  • Límites de retención

Implementación Práctica

Winston (Node.js)

const winston = require('winston');

// Función de sanitización
const sanitizeFormat = winston.format((info) => {
  const sensitiveKeys = ['password', 'token', 'apiKey', 'authorization'];
  
  const sanitize = (obj) => {
    if (typeof obj !== 'object' || obj === null) return obj;
    
    const result = Array.isArray(obj) ? [] : {};
    for (const [key, value] of Object.entries(obj)) {
      if (sensitiveKeys.some(k => key.toLowerCase().includes(k))) {
        result[key] = '[REDACTED]';
      } else if (typeof value === 'object') {
        result[key] = sanitize(value);
      } else {
        result[key] = value;
      }
    }
    return result;
  };
  
  return sanitize(info);
});

// Crear logger
const logger = winston.createLogger({
  level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
  format: winston.format.combine(
    sanitizeFormat(),
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.Console(),
    // En producción, agregar transports a sistemas centralizados
  ]
});

// Uso
logger.info('User login', { userId: 'u1234', ip: '192.168.1.1' });
logger.debug('Full request', { body: req.body }); // Solo en dev

Pino (Node.js - más rápido)

import pino from 'pino';

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
  redact: {
    paths: ['password', 'token', '*.password', '*.token', 'headers.authorization'],
    censor: '[REDACTED]'
  },
  // Solo en desarrollo
  transport: process.env.NODE_ENV !== 'production' 
    ? { target: 'pino-pretty' } 
    : undefined
});

logger.info({ event: 'user_login', userId: 'u1234' });

Logging Condicional por Ambiente

// Opción simple
if (process.env.NODE_ENV !== 'production') {
  console.log('Debug:', data);
}

// Mejor: usar una función wrapper
const debug = (...args) => {
  if (process.env.NODE_ENV !== 'production') {
    console.log('[DEBUG]', ...args);
  }
};

debug('User object:', user);

Automatización: Prevenir console.log en Producción

ESLint

// .eslintrc.js
module.exports = {
  rules: {
    'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'warn'
  }
};

Pre-commit Hook (Husky)

# .husky/pre-commit
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

# Buscar console.log en archivos staged
if git diff --cached --name-only | xargs grep -l 'console.log' 2>/dev/null; then
  echo "❌ Error: console.log encontrado en archivos staged"
  echo "Usa el logger estructurado en lugar de console.log"
  exit 1
fi

CI/CD Gate

# .github/workflows/lint.yml
- name: Check for console.log
  run: |
    if grep -r "console.log" --include="*.js" --include="*.ts" src/; then
      echo "::error::console.log encontrado en código fuente"
      exit 1
    fi

Checklist de Logging Seguro

Resumen de Acciones

## Nunca Loguear
- [ ] Passwords / credentials
- [ ] Tokens JWT / API keys  
- [ ] PII (email, nombre, dirección)
- [ ] Datos financieros
- [ ] Variables de entorno sensibles

## Implementar
- [ ] Función de sanitización centralizada
- [ ] Log levels por ambiente (debug solo en dev)
- [ ] JSON estructurado en lugar de texto libre
- [ ] Framework de logging (Winston/Pino/Bunyan)

## Configurar
- [ ] Retención de logs (30-90 días máx)
- [ ] Rotación automática
- [ ] RBAC en sistemas de logs
- [ ] Encriptación en tránsito

## Automatizar
- [ ] ESLint rule: no-console
- [ ] Pre-commit hook
- [ ] CI/CD gate
- [ ] Scanner de datos sensibles

Herramientas Recomendadas

Herramienta Tipo Uso
Winston Logger Node.js Más popular, muchos transports
Pino Logger Node.js Más rápido, redaction built-in
Bunyan Logger Node.js JSON by default
ESLint Linter Regla no-console
Husky Git hooks Pre-commit validation
Datadog SaaS Log aggregation + sensitive data scanner
AWS CloudWatch Cloud Logs con retention policies

Caso de Estudio: La Fuga Silenciosa

El Setup

Una fintech construye un dashboard para préstamos. Durante desarrollo:

// Frontend
console.log("Loan request payload:", requestBody);

// Backend  
console.log("Loan response:", response);

Ambos “temporales” para debugging.

El Incidente

Seis meses después, la app está en producción. Los logs — ahora en un sistema centralizado — contienen:

{
  "loanAmount": 50000,
  "userEmail": "cliente@empresa.com",
  "bankAccount": "******9821",
  "ssn": "123-45-6789"
}

Un permiso mal configurado en el log viewer dio acceso read-only a contratistas externos.

Por varios meses, datos financieros personales estuvieron visibles para terceros.

Resultado: Notificación de breach bajo GDPR, multa potencial, daño reputacional.

La Solución

  1. Reemplazar todos los console.log con Winston estructurado
  2. Mascarar campos sensibles en ingestion
  3. Política de retención de 30 días
  4. Regla ESLint para bloquear console.log() en producción

Conclusión

console.log() no es el enemigo. El logging descontrolado sí lo es.

La diferencia entre console.log() y logging seguro no es sintaxis — es intención. Uno es conveniencia temporal; el otro es observabilidad deliberada.

El software moderno corre en ambientes distribuidos, monitoreados, y frecuentemente regulados. Cada línea que imprimes se convierte en un registro que alguien puede leer — o abusar.

La próxima vez que uses console.log(), pregúntate:

“¿Estaría bien si estos datos terminaran en un dashboard público?”

Si no — ya sabes qué hacer.

Loguea menos. Loguea inteligente. Haz tu consola más silenciosa — pero mucho más segura.


Recursos


Publicado en yoDEV.dev — La comunidad de desarrolladores de Latinoamérica