Seguridad en Proyectos
Se observa últimamente muchas aplicaciones web con vibe-coding, pero se deja de lado la seguridad. ¡Veamos algunas formas básicas de asegurar nuestras páginas!
xWickz - 4 de mayo
Hemos visto últimamente muchas aplicaciones web o incluso software que están hechos con vibe-coding, lo cual no tiene nada de malo... Cada quien con su criterio! Sin embargo, se está descuidando lo más importante.
La seguridad.
Priorizar la seguridad en nuestros proyectos es fundamental y me atrevería a decir que es obligatorio para garantizar que no solo nuestros usuarios estén protegidos, sino también para salvaguardar nuestra propia aplicación de posibles ataques o vulnerabilidades.
Por ejemplo, exponer nuestras API Keys o no validar correctamente los datos.
Esto se acaba, hoy.
Te mostraré algunas formas de asegurar de forma básica tu sitio web o proyecto, para que no tengas que preocuparte por eso!
Validación de Entradas y Sanitización
Si tienes una aplicación web, es probable que los usuarios deban ingresar ciertos datos, llamadas hacia la API, un formulario. Es vital proteger los datos no solo que envian los usuarios sino que recibe el servidor para evitar ataques como inyección SQL o cross-site scripting (XSS). Para esto, es importante validar y sanitizar los datos de entrada.
Validación de Entradas
Para validar tienes una librería muy famosa que es Zod. Con ella puedes definir esquemas y validación de datos tanto del lado del cliente como del servidor. Un ejemplo básico:
import { z } from 'zod';
// 1. Definimos el esquema de validación
const ContactFormSchema = z.object({
name: z.string().min(2, "El nombre debe tener al menos 2 caracteres").max(50),
email: z.string().email("Formato de correo electrónico inválido"),
message: z.string().min(10, "El mensaje debe tener al menos 10 caracteres").max(500),
});
// 2. Función para procesar los datos (en el backend o API Route)
export async function POST(req: Request) {
const body = await req.json();
// Validamos los datos
const result = ContactFormSchema.safeParse(body);
if (!result.success) {
// Retornamos los errores específicos de Zod
return Response.json({ errors: result.error.flatten().fieldErrors }, { status: 400 });
}
// Aquí los datos ya son seguros y tienen el formato correcto
return Response.json({ success: true, data: result.data });
}
Sanitización de Datos
Imagina que tienes un formulario de comentarios en tu sitio web. Un usuario malintencionado podría intentar inyectar código malicioso, como un script, para robar información de otros usuarios o comprometer la seguridad de tu aplicación. Para evitar esto, es crucial sanitizar los datos de entrada antes de procesarlos o almacenarlos. O por ejemplo, si este mismo blog permitiera a los usuarios dejar comentarios, sería importante sanitizar esos comentarios para evitar que alguien intente inyectar código malicioso.
Para esto, puedes usar librerías como DOMPurify o sanitize-html. Estas librerías eliminan cualquier código potencialmente peligroso de los datos de entrada. Un ejemplo básico, te va:
Instalación
npm install isomorphic-dompurify
Ejemplo de Uso en un Componente Básico de React
import React from 'react';
import DOMPurify from 'isomorphic-dompurify';
export default function ArticleContent({ rawHtml }: { rawHtml: string }) {
// Sanitizamos el HTML para eliminar scripts o atributos peligrosos (como onclick)
const cleanHtml = DOMPurify.sanitize(rawHtml, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br', 'h2', 'h3'],
ALLOWED_ATTR: ['href', 'target', 'rel'],
});
return (
<article
className="prose dark:prose-invert"
dangerouslySetInnerHTML={{ __html: cleanHtml }}
/>
);
}
Pero claro, imaginemos que tenemos una API que recibe datos a través de una API route y quereos guardarlos limpios en la base de datos (por ejemplo, contenido de texto enriquecido o markdown parseado), es mejor hacerlo en el servidor. Aquí un ejemplo:
Instalación
npm install sanitize-html
Ejemplo de uso en una API Route (Next.js):
import { NextRequest, NextResponse } from 'next/server';
import sanitizeHtml from 'sanitize-html';
export async function POST(req: NextRequest) {
try {
const body = await req.json();
const { dirtyContent } = body;
// Sanitizamos el texto recibido
const cleanContent = sanitizeHtml(dirtyContent, {
allowedTags: ['b', 'i', 'em', 'strong', 'p', 'span'],
allowedAttributes: {
'span': ['style'] // Permite ciertos estilos si lo necesitas
},
disallowedTagsMode: 'discard' // Elimina etiquetas peligrosas o no permitidas
});
// cleanContent ahora no contiene scripts ni tags maliciosos.
// Ya puedes guardarlo en tu base de datos de forma segura.
return NextResponse.json({
success: true,
cleaned: cleanContent
});
} catch (error) {
return NextResponse.json({ error: 'Error procesando el contenido' }, { status: 500 });
}
}
Ahora, si solo necesitas limpiar un campo de texto simple (como un nombre o un comentario corto) para evitar que ingresen saltos de línea extraños o caracteres de control, puedes usar una función nativa sencilla junto con la validación de Zod:
function sanitizeSimpleString(input: string): string {
return input
.trim() // Elimina espacios al principio y final
.replace(/[<>]/g, '') // Elimina los signos menor y mayor que (< >) para romper la inyección de tags HTML
.slice(0, 255); // Limita la longitud máxima
}
// Ejemplo
const nombreSucio = "<script>alert('hack')</script> Carlos ";
const nombreLimpio = sanitizeSimpleString(nombreSucio);
// Resultado: "scriptalert('hack')/script Carlos"
Protección de Formularios y Bots
Evita que tus formularios sean spameados por bots o utilizados para realizar ataques de fuerza bruta. Imagina que, en tu sitio web tienes una parte de formulario dónde los usuarios pueden registrarse o iniciar sesión. Sin protección, un bot podría intentar adivinar contraseñas o crear cuentas falsas.
reCAPTCHA
Para esto, puedes implementar medidas como CAPTCHA o reCAPTCHA de Google para asegurarte de que solo humanos puedan interactuar con tus formularios.
Implementación Básica de reCAPTCHA en un Formulario de Next.js:
"use client";
import { useRef, useState } from "react";
import ReCAPTCHA from "react-google-recaptcha";
export function Formulario() {
const recaptchaSiteKey = process.env.NEXT_PUBLIC_RECAPTCHA_SITE_KEY ?? "";
const recaptchaRef = useRef<ReCAPTCHA>(null);
const [status, setStatus] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setStatus("Enviando...");
// 1. Obtenemos el valor del captcha
const captchaValue = recaptchaRef.current?.getValue();
if (!captchaValue) {
setStatus("Por favor, completa el reCAPTCHA.");
return;
}
try {
// 2. Enviamos el token a nuestra API
const response = await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ captchaToken: captchaValue }),
});
if (response.ok) {
setStatus("¡Enviado con éxito!");
} else {
setStatus("Ocurrió un error.");
}
} catch (error) {
setStatus("Error de conexión.");
} finally {
// 3. Reiniciamos el captcha para permitir otro intento
recaptchaRef.current?.reset();
}
};
return (
<form onSubmit={handleSubmit} className="p-6 border rounded-lg max-w-md">
<h2 className="text-lg font-bold mb-4">Formulario de Contacto</h2>
<div className="mb-4 flex justify-center">
<ReCAPTCHA sitekey={recaptchaSiteKey} ref={recaptchaRef} />
</div>
<button
type="submit"
className="w-full bg-blue-600 text-white py-2 rounded hover:bg-blue-700 transition"
>
Enviar
</button>
{status && <p className="mt-4 text-sm text-center">{status}</p>}
</form>
);
}
¡Pero no acaba aqui! reCAPTCHA tiene muchas más formas de usarse, como la versión invisible o la v3 que no requiere interacción del usuario, pero esta es la forma más basica y común de implementarlo.
Limitación de Intentos (Rate Limiting con Redis)
Imagina que tienes algún listillo que te quiere saturar la API de peticiones, o algún bot que intenta adivinar contraseñas en tu formulario de login. Para evitar esto, puedes implementar una limitación de intentos (rate limiting) utilizando Redis para almacenar el número de intentos por IP o por usuario. Aquí un ejemplo basico de cómo hacerlo en una API Route de Next.js:
"use server";
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";
import { headers } from "next/headers";
// 1. Configuramos Redis y el Rate Limit (Ejemplo: Máximo 3 peticiones por día)
const rateLimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(3, "1 d"),
});
export async function sendContactForm(values: any) {
// 2. Obtenemos la IP del usuario que hace la petición
const ip = (await headers()).get("x-forwarded-for") || "127.0.0.1";
// 3. Verificamos el límite en Redis
const { success: limitOk } = await rateLimit.limit(ip);
if (!limitOk) {
console.log(`Rate limit exceeded for IP: ${ip}`);
return { error: "Too many requests, please try again later." };
}
try {
// tu logica
return { success: true };
} catch (error) {
return { error: "Error al procesar la petición." };
}
}
HoneyPot
Otra técnica sencilla para detectar bots es el uso de un campo oculto (honeypot) en tus formularios. Este campo no debe ser visible para los usuarios humanos, pero los bots suelen llenarlo automáticamente. Si el campo tiene algún valor, puedes asumir que la solicitud proviene de un bot y rechazarla.
"use client";
import { useState } from "react";
// 1. Formulario Básico
export function FormularioCompacto() {
const [nombre, setNombre] = useState("");
const [email, setEmail] = useState("");
const [website, setWebsite] = useState(""); // Honeypot: Campo trampa
const [status, setStatus] = useState("");
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setStatus("Enviando...");
const response = await fetch("/api/contacto", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ nombre, email, website }),
});
if (response.ok) setStatus("¡Enviado con éxito!");
else setStatus("Error al enviar.");
};
return (
<form onSubmit={handleSubmit} className="p-6 border rounded-lg max-w-sm mx-auto mt-10 space-y-4">
<h2 className="text-lg font-bold">Contacto</h2>
<div>
<label className="text-sm block mb-1">Nombre</label>
<input
type="text"
value={nombre}
onChange={(e) => setNombre(e.target.value)}
className="w-full p-2 border rounded"
required
/>
</div>
<div>
<label className="text-sm block mb-1">Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full p-2 border rounded"
required
/>
</div>
{/* Campo trampa oculto para los bots (Honeypot) */}
<div className="absolute left-[-9999px]" aria-hidden="true">
<input
type="text"
value={website}
onChange={(e) => setWebsite(e.target.value)}
tabIndex={-1}
/>
</div>
<button type="submit" className="w-full bg-blue-600 text-white p-2 rounded">
Enviar
</button>
{status && <p className="text-sm text-center">{status}</p>}
</form>
);
}
// 2. Ruta API para procesar el formulario y detectar bots
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
try {
const body = await req.json();
// Verificación del honeypot: Si tiene contenido, es un bot
if (body.website && body.website.length > 0) {
console.warn("🤖 Bot detectado por el Honeypot");
return NextResponse.json({ success: true }); // Respuesta silenciosa
}
// Lógica normal
return NextResponse.json({ success: true, message: "Datos procesados" });
} catch (error) {
return NextResponse.json({ error: "Error en el servidor" }, { status: 500 });
}
}
Secretos y Sesiones
- Nunca guardes API keys en el codigo. Usa variables de entorno y permisos minimos.
- Para logins, guarda contraseñas con hash (bcrypt/argon2) y usa cookies
HttpOnly+Securepara sesiones.
Dependencias
Mantener dependencias al dia evita vulnerabilidades conocidas. Haz revisiones periodicas con pnpm audit o el equivalente.
HTTPS Siempre
Sirve tu sitio solo por HTTPS y redirecciona todo lo que venga por HTTP. Esto evita que alguien intercepte sesiones o datos sensibles. Si puedes, habilita HSTS (estrictamente HTTPS).
Headers de Seguridad
Headers simples ayudan mucho sin tocar el frontend. En Next.js puedes agregarlos así:
// next.config.ts
const securityHeaders = [
{ key: "X-Content-Type-Options", value: "nosniff" },
{ key: "X-Frame-Options", value: "DENY" },
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{ key: "Content-Security-Policy", value: "default-src 'self'" },
];
export default {
async headers() {
return [
{
source: "/(.*)",
headers: securityHeaders,
},
];
},
};
Conclusión
Hemos visto ahora como poder asegurar de forma básica una aplicación. Hay muchísimas más formas de hacerlo, todo depende de qué trate tu aplicación.
Espero que este artículo te haya servido para una noción básica.
Un saludo.