👨‍💻
DesarrolloBuenas PrácticasSeguridad WebTutorial

Guía de Seguridad para Desarrolladores: 20 Prácticas Esenciales

Checklist práctica de seguridad que todo desarrollador debe implementar desde el día uno. Con ejemplos de código y herramientas.

📅20 de noviembre de 2025
👤Pentacode
⏱️15 min de lectura

Guía de Seguridad para Desarrolladores: 20 Prácticas Esenciales

La seguridad no es responsabilidad solo del equipo de seguridad. Es responsabilidad de cada desarrollador que escribe código.

Esta guía te da 20 prácticas concretas, con código de ejemplo, que puedes implementar hoy mismo.

1. Nunca Confíes en Input del Usuario

Regla de oro: Toda entrada es maliciosa hasta que se demuestre lo contrario.

Ejemplo Vulnerable:

JavaScript
// ❌ VULNERABLE
app.get('/search', (req, res) => {
  const query = `SELECT * FROM products WHERE name LIKE '%${req.query.term}%'`
  const results = db.query(query)
  res.json(results)
})

// Ataque: /search?term='; DROP TABLE products; --

Ejemplo Seguro:

JavaScript
// ✅ SEGURO - Query parametrizada
app.get('/search', async (req, res) => {
  const term = req.query.term

  // Validación
  if (!term || typeof term !== 'string' || term.length > 100) {
    return res.status(400).json({ error: 'Término de búsqueda inválido' })
  }

  // Sanitización
  const sanitized = term.trim()

  // Query parametrizada (previene SQL injection)
  const results = await db.query(
    'SELECT * FROM products WHERE name LIKE ?',
    [`%${sanitized}%`]
  )

  res.json(results)
})

Herramientas:

  • Validación: zod, joi, yup
  • Sanitización: validator.js, DOMPurify (frontend)

2. Usa Queries Parametrizadas SIEMPRE

❌ String Concatenation:

JavaScript
// Vulnerable a SQL Injection
const query = `SELECT * FROM users WHERE id = ${userId}`

✅ Prepared Statements:

JavaScript
// Node.js con diferentes ORMs/drivers

// Con mysql2
const [rows] = await connection.execute(
  'SELECT * FROM users WHERE id = ?',
  [userId]
)

// Con PostgreSQL (pg)
const result = await client.query(
  'SELECT * FROM users WHERE id = $1',
  [userId]
)

// Con Prisma (ORM)
const user = await prisma.user.findUnique({
  where: { id: userId }  // Automáticamente parametrizado
})

// Con TypeORM
const user = await userRepository.findOne({
  where: { id: userId }  // Automáticamente parametrizado
})

3. Hash de Contraseñas Correctamente

❌ NUNCA hagas esto:

JavaScript
// Texto plano
user.password = req.body.password

// MD5/SHA1 (obsoletos y rápidos de crackear)
user.password = crypto.createHash('md5').update(password).digest('hex')

// SHA256 sin salt
user.password = crypto.createHash('sha256').update(password).digest('hex')

✅ Usa bcrypt o Argon2:

JavaScript
import bcrypt from 'bcrypt'

// Al crear/actualizar contraseña
const saltRounds = 12  // Más rounds = más seguro pero más lento
const hashedPassword = await bcrypt.hash(password, saltRounds)

// Al verificar contraseña
const isValid = await bcrypt.compare(plainPassword, hashedPassword)

// Con Argon2 (más moderno)
import argon2 from 'argon2'

const hashedPassword = await argon2.hash(password)
const isValid = await argon2.verify(hashedPassword, plainPassword)

Por qué bcrypt/Argon2:

  • Diseñados para ser lentos (previenen fuerza bruta)
  • Salt automático (cada hash es único)
  • Adaptables (puedes aumentar rounds con el tiempo)

4. Implementa Autenticación Robusta

Checklist de Autenticación:

JavaScript
// Login endpoint seguro
app.post('/api/auth/login',
  rateLimiter,  // Rate limiting
  async (req, res) => {
    const { email, password } = req.body

    // 1. Validación de input
    if (!email || !password) {
      return res.status(400).json({ error: 'Campos requeridos' })
    }

    // 2. Buscar usuario (usa timing-safe comparison)
    const user = await db.users.findByEmail(email)

    // 3. Verificar contraseña
    if (!user || !(await bcrypt.compare(password, user.password))) {
      // Mismo mensaje para ambos casos (no revelar si el email existe)
      await logSecurityEvent('login_failed', { email, ip: req.ip })
      return res.status(401).json({ error: 'Credenciales inválidas' })
    }

    // 4. Verificar estado de la cuenta
    if (user.isBanned) {
      return res.status(403).json({ error: 'Cuenta suspendida' })
    }

    if (user.requiresEmailVerification && !user.emailVerified) {
      return res.status(403).json({ error: 'Verifica tu email primero' })
    }

    // 5. Generar tokens
    const accessToken = generateJWT(user, '15m')
    const refreshToken = generateJWT(user, '7d')

    // 6. Guardar refresh token en DB (para poder revocar)
    await db.refreshTokens.create({
      userId: user.id,
      token: refreshToken,
      expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
    })

    // 7. Log exitoso
    await logSecurityEvent('login_success', {
      userId: user.id,
      ip: req.ip
    })

    // 8. Actualizar último login
    await db.users.update(user.id, {
      lastLoginAt: new Date(),
      lastLoginIp: req.ip
    })

    res.json({ accessToken, refreshToken })
  }
)

5. Protege tus JWT

❌ Malas prácticas:

JavaScript
// Secret débil
const secret = 'secret'

// Sin expiración
const token = jwt.sign({ userId: user.id }, secret)

// Datos sensibles en payload
const token = jwt.sign({
  userId: user.id,
  password: user.password,  // ❌ NUNCA
  creditCard: user.card     // ❌ NUNCA
}, secret)

✅ Buenas prácticas:

JavaScript
import jwt from 'jsonwebtoken'

// Secret fuerte (guardado en variable de entorno)
const JWT_SECRET = process.env.JWT_SECRET  // Al menos 32 caracteres random

// Función para generar tokens
function generateJWT(user, expiresIn = '15m') {
  const payload = {
    sub: user.id,  // Subject (user ID)
    email: user.email,
    role: user.role,
    iat: Math.floor(Date.now() / 1000),  // Issued at
    // NO incluir datos sensibles
  }

  return jwt.sign(payload, JWT_SECRET, {
    expiresIn,  // Token expira
    issuer: 'mi-app.com',  // Quién emitió el token
    audience: 'mi-app.com'  // Para quién es el token
  })
}

// Middleware para verificar tokens
function authenticateJWT(req, res, next) {
  const authHeader = req.headers.authorization

  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Token no proporcionado' })
  }

  const token = authHeader.substring(7)  // Remover "Bearer "

  try {
    const decoded = jwt.verify(token, JWT_SECRET, {
      issuer: 'mi-app.com',
      audience: 'mi-app.com'
    })

    req.user = decoded
    next()
  } catch (err) {
    if (err.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expirado' })
    }
    return res.status(401).json({ error: 'Token inválido' })
  }
}

6. Implementa Control de Acceso en Cada Endpoint

Patrón de Middleware:

JavaScript
// middleware/authorize.js
export function authorize(...allowedRoles) {
  return async (req, res, next) => {
    // Usuario debe estar autenticado primero
    if (!req.user) {
      return res.status(401).json({ error: 'No autenticado' })
    }

    // Verificar rol
    if (!allowedRoles.includes(req.user.role)) {
      await logSecurityEvent('access_denied', {
        userId: req.user.id,
        resource: req.path,
        requiredRoles: allowedRoles,
        userRole: req.user.role
      })

      return res.status(403).json({ error: 'No autorizado' })
    }

    next()
  }
}

// Uso en rutas
app.get('/api/admin/users',
  authenticateJWT,
  authorize('admin'),  // Solo admins
  async (req, res) => {
    // Código del endpoint
  }
)

app.delete('/api/posts/:postId',
  authenticateJWT,
  authorize('admin', 'moderator'),  // Admins o moderators
  async (req, res) => {
    // Código del endpoint
  }
)

Verificación de Ownership:

JavaScript
// middleware/checkOwnership.js
export function checkOwnership(resourceType) {
  return async (req, res, next) => {
    const resourceId = req.params[`${resourceType}Id`]
    const userId = req.user.id

    let resource
    switch (resourceType) {
      case 'post':
        resource = await db.posts.findById(resourceId)
        break
      case 'comment':
        resource = await db.comments.findById(resourceId)
        break
      // ... más recursos
    }

    if (!resource) {
      return res.status(404).json({ error: 'Recurso no encontrado' })
    }

    // Verificar ownership o admin
    if (resource.userId !== userId && req.user.role !== 'admin') {
      return res.status(403).json({ error: 'No puedes modificar este recurso' })
    }

    req.resource = resource  // Pasar recurso al handler
    next()
  }
}

// Uso
app.patch('/api/posts/:postId',
  authenticateJWT,
  checkOwnership('post'),
  async (req, res) => {
    // req.resource ya está verificado como propiedad del usuario
    await updatePost(req.resource.id, req.body)
    res.json({ success: true })
  }
)

7. Rate Limiting en Todos los Endpoints Sensibles

Implementación con express-rate-limit:

JavaScript
import rateLimit from 'express-rate-limit'
import RedisStore from 'rate-limit-redis'
import Redis from 'ioredis'

const redis = new Redis(process.env.REDIS_URL)

// Rate limiter para login
const loginLimiter = rateLimit({
  store: new RedisStore({
    client: redis,
    prefix: 'rl:login:'
  }),
  windowMs: 15 * 60 * 1000,  // 15 minutos
  max: 5,  // 5 intentos
  message: 'Demasiados intentos de login. Intenta en 15 minutos.',
  standardHeaders: true,
  legacyHeaders: false,
  handler: (req, res) => {
    logSecurityEvent('rate_limit_exceeded', {
      endpoint: '/login',
      ip: req.ip,
      email: req.body.email
    })

    res.status(429).json({
      error: 'Demasiados intentos',
      retryAfter: req.rateLimit.resetTime
    })
  }
})

// Rate limiter para API general
const apiLimiter = rateLimit({
  windowMs: 1 * 60 * 1000,  // 1 minuto
  max: 100,  // 100 requests por minuto
  message: 'Demasiadas peticiones'
})

// Rate limiter para operaciones costosas
const heavyOperationLimiter = rateLimit({
  windowMs: 60 * 60 * 1000,  // 1 hora
  max: 10,  // 10 operaciones por hora
  keyGenerator: (req) => {
    // Limitar por usuario autenticado, no solo IP
    return req.user ? req.user.id : req.ip
  }
})

// Aplicar en rutas
app.post('/api/auth/login', loginLimiter, loginHandler)
app.use('/api/', apiLimiter)  // Rate limit general
app.post('/api/reports/generate',
  authenticateJWT,
  heavyOperationLimiter,
  generateReportHandler
)

8. Valida y Sanitiza TODOS los Inputs

Con Zod (TypeScript):

TypeScript
import { z } from 'zod'

// Schema de validación
const createUserSchema = z.object({
  email: z.string().email('Email inválido').toLowerCase(),
  password: z.string()
    .min(8, 'Mínimo 8 caracteres')
    .regex(/[A-Z]/, 'Debe contener mayúscula')
    .regex(/[a-z]/, 'Debe contener minúscula')
    .regex(/[0-9]/, 'Debe contener número')
    .regex(/[^A-Za-z0-9]/, 'Debe contener carácter especial'),
  firstName: z.string()
    .min(1, 'Nombre requerido')
    .max(50, 'Máximo 50 caracteres')
    .regex(/^[a-zA-ZáéíóúÁÉÍÓÚñÑ\s]+$/, 'Solo letras y espacios'),
  age: z.number()
    .int('Debe ser entero')
    .min(18, 'Debes ser mayor de 18')
    .max(120, 'Edad inválida'),
  website: z.string().url('URL inválida').optional(),
  role: z.enum(['user', 'admin', 'moderator']).default('user')
})

// Middleware de validación
function validate(schema: z.ZodSchema) {
  return (req, res, next) => {
    try {
      req.validatedData = schema.parse(req.body)
      next()
    } catch (error) {
      if (error instanceof z.ZodError) {
        return res.status(400).json({
          error: 'Validación fallida',
          details: error.errors.map(err => ({
            field: err.path.join('.'),
            message: err.message
          }))
        })
      }
      next(error)
    }
  }
}

// Uso en rutas
app.post('/api/users',
  validate(createUserSchema),
  async (req, res) => {
    // req.validatedData contiene datos validados y sanitizados
    const user = await createUser(req.validatedData)
    res.json(user)
  }
)

9. Configura Headers de Seguridad

Con Helmet.js:

JavaScript
import helmet from 'helmet'

app.use(helmet({
  // Content Security Policy
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "'unsafe-inline'", "trusted-cdn.com"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", "data:", "https:"],
      connectSrc: ["'self'", "api.miapp.com"],
      fontSrc: ["'self'", "fonts.gstatic.com"],
      objectSrc: ["'none'"],
      mediaSrc: ["'self'"],
      frameSrc: ["'none'"]
    }
  },

  // HSTS (HTTPS obligatorio)
  hsts: {
    maxAge: 31536000,  // 1 año
    includeSubDomains: true,
    preload: true
  },

  // Prevenir clickjacking
  frameguard: {
    action: 'deny'
  },

  // Prevenir MIME sniffing
  noSniff: true,

  // Referrer Policy
  referrerPolicy: {
    policy: 'strict-origin-when-cross-origin'
  }
}))

// Headers adicionales
app.use((req, res, next) => {
  // Prevenir caching de respuestas sensibles
  if (req.path.startsWith('/api/')) {
    res.set('Cache-Control', 'no-store, no-cache, must-revalidate, private')
  }

  // Remover header que revela tecnología
  res.removeHeader('X-Powered-By')

  next()
})

10. Maneja Secretos Correctamente

❌ NUNCA hagas esto:

JavaScript
// Hardcoded en código
const apiKey = 'sk_live_12345abcde'

// En el código fuente
const dbPassword = 'MyP@ssw0rd123'

// Commiteado en Git
git add .env

✅ Usa variables de entorno:

JavaScript
// .env (NUNCA commitear este archivo)
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
JWT_SECRET=muy-secreto-y-largo-al-menos-32-caracteres
STRIPE_API_KEY=sk_live_51abcdefg...
SENDGRID_API_KEY=SG.abc123...

// .env.example (SÍ commitear este archivo)
DATABASE_URL=
JWT_SECRET=
STRIPE_API_KEY=
SENDGRID_API_KEY=

// .gitignore
.env
.env.local
.env.*.local

// Usar en código
import dotenv from 'dotenv'
dotenv.config()

const stripeKey = process.env.STRIPE_API_KEY

if (!stripeKey) {
  throw new Error('STRIPE_API_KEY no configurada')
}

Para producción, usa servicios de gestión de secretos:

  • AWS Secrets Manager
  • Google Cloud Secret Manager
  • HashiCorp Vault
  • Azure Key Vault

Checklist Rápida de Seguridad

Antes de cada deployment:

Input & Validation

  • Todos los inputs validados con schemas
  • Queries parametrizadas (no string concatenation)
  • Sanitización de HTML en inputs de texto rico
  • Validación de tipos y rangos

Autenticación & Autorización

  • Contraseñas hasheadas con bcrypt/Argon2
  • JWTs con expiración corta (< 30min)
  • Refresh tokens implementados
  • Rate limiting en login/signup
  • Verificación de permisos en cada endpoint

APIs & Endpoints

  • Rate limiting configurado
  • CORS configurado correctamente
  • Headers de seguridad (helmet)
  • No hay información sensible en responses
  • Errores no revelan detalles internos

Data Protection

  • HTTPS en producción
  • Secretos en variables de entorno
  • Datos sensibles cifrados en DB
  • Backups automáticos configurados

Logging & Monitoring

  • Eventos de seguridad registrados
  • Logs no contienen datos sensibles
  • Monitoreo de actividad sospechosa
  • Alertas configuradas

Dependencies

  • Dependencias actualizadas
  • npm audit sin vulnerabilidades críticas
  • Dependencias no utilizadas removidas

Herramientas Esenciales

Para Validación:

  • Zod (TypeScript): Schema validation
  • Joi (JavaScript): Schema validation
  • Validator.js: String validation y sanitización

Para Autenticación:

  • Passport.js: Estrategias de auth
  • jsonwebtoken: JWT
  • bcrypt: Hashing de contraseñas

Para Security Headers:

  • Helmet.js: Security headers automáticos

Para Rate Limiting:

  • express-rate-limit: Rate limiting simple
  • rate-limiter-flexible: Rate limiting avanzado con Redis

Para Auditoría:

  • npm audit: Vulnerabilidades en dependencias
  • Snyk: Monitoreo continuo de vulnerabilidades
  • OWASP Dependency-Check: Análisis de dependencias

Para Testing:

  • OWASP ZAP: Pentest automático
  • Burp Suite: Análisis de tráfico HTTP
  • SQLMap: Testing de SQL injection

Conclusión

La seguridad no es un feature que agregas al final. Es una mentalidad que aplicas desde la primera línea de código.

Implementa estas 20 prácticas y estarás en el top 10% de aplicaciones más seguras.

¿Quieres validar que tu código es realmente seguro? En Pentacode realizamos pentests especializados para desarrolladores, con reportes técnicos y recomendaciones específicas de código. Agenda una consultoría.


Próximos pasos:

Compartir artículo:

🛡️

¿Listo para proteger tu aplicación?

Agenda una consultoría gratuita y descubre cómo nuestros servicios de pentesting pueden ayudarte a identificar vulnerabilidades.

Chatea con nosotros