Buenas prácticas: dependencias, secretos, validación y superficie SSR
La higiene de seguridad que sostiene todo lo anterior: auditar la cadena de suministro de dependencias, resguardar secretos con astro:env y la frontera servidor-cliente para no filtrarlos al navegador, validar todo input externo con Zod bajo el lema parsea no valides, y mapear la superficie de ataque específica que el render bajo demanda añade a un sitio.
Las lecciones anteriores blindaron puntos concretos —el XSS, la CSP, las cabeceras, el CSRF—, pero la seguridad de un sitio no es la suma de sus parches: es una propiedad del sistema entero, y se pierde por el eslabón que nadie miró. Esta lección cierra el nivel con la higiene transversal que sostiene todo lo demás: vigilar el código ajeno del que dependes, guardar los secretos donde el navegador no los vea, no confiar en ningún dato que entre de fuera, y entender que servir bajo demanda abre una superficie de ataque que el sitio estático no tenía. No son trucos, son hábitos; y los hábitos son lo que de verdad decide si un sistema resiste.
- Auditar la cadena de suministro de dependencias y reducir el riesgo de código ajeno.
- Resguardar secretos con
astro:envrespetando la frontera servidor-cliente. - Validar todo input externo con Zod bajo el principio de parsea, no valides.
- Mapear la superficie de ataque que el render bajo demanda añade frente a lo estático.
Dependencias: la cadena de suministro
La mayor parte del código que se despliega en tu sitio no lo escribiste tú: son dependencias, y cada una es confianza depositada en un extraño. Un paquete comprometido —secuestrado, con una versión maliciosa publicada— ejecuta su código en tu build o en tu servidor con tus permisos. La primera defensa es barata y hay que ritualizarla: auditar el árbol de dependencias contra las vulnerabilidades conocidas.
pnpm audit --audit-level=high
pnpm outdated
El audit cruza tu árbol con las bases de datos de vulnerabilidades y te dice qué actualizar; el fichero de bloqueo —el lockfile— fija las versiones exactas para que un install no arrastre en silencio algo nuevo. Sobre esa base, la higiene es cultural: minimizar el número de dependencias —cada una es superficie—, preferir las mantenidas y de origen claro, automatizar las alertas con un bot que abra pull requests de parche, y desconfiar del paquete que pide más permisos o scripts de instalación de los que su función justifica. El código que no incluyes es el único que no te puede traicionar.
Secretos: astro:env y la frontera servidor-cliente
Un secreto —una clave de API, la cadena de la base de datos— filtrado al navegador es un secreto quemado, porque cualquiera lee el JavaScript que sirves. La regla es tajante: los secretos viven y mueren en el servidor. Astro 7 te da un contrato tipado para no equivocarte, astro:env, donde declaras cada variable con su contexto —servidor o cliente— y su acceso —secreto o público—.
// astro.config.mjs
import { defineConfig, envField } from 'astro/config';
export default defineConfig({
env: {
schema: {
API_SECRET: envField.string({ context: 'server', access: 'secret' }),
PUBLIC_API_URL: envField.string({ context: 'client', access: 'public' }),
},
},
});
Con ese esquema, importas el secreto desde astro:env/server y Astro garantiza que jamás acabe en el bundle del cliente: si por error intentas usar en el navegador algo marcado como server, el build falla en vez de filtrar. Es la frontera servidor-cliente convertida en una regla que el compilador vigila por ti.
import { API_SECRET } from 'astro:env/server';
// disponible solo en codigo de servidor; nunca viaja al navegador
Lo público —lo que sí puede ver el cliente— se declara aparte y con el prefijo que lo delata, de modo que la distinción esté escrita en el nombre. Nunca metas un secreto en un PUBLIC_, en un define:vars, ni en ningún valor que se serialice al HTML: todo eso es territorio del navegador, y el navegador es un lugar público.
Validación con Zod: parsea, no valides
Todo dato que entra de fuera —el cuerpo de un formulario, un parámetro de la URL, la respuesta de una API, una cabecera— es hostil hasta que se demuestre lo contrario. No basta con comprobar que parece válido; el enfoque robusto es parsear: transformar el input crudo en un valor tipado y de forma conocida, o rechazarlo de plano. Zod es la herramienta idiomática, y las actions de Astro la llevan integrada en su input.
import { z } from 'astro:schema';
const esquema = z.object({
email: z.string().email(),
edad: z.coerce.number().int().min(18).max(120),
});
const resultado = esquema.safeParse(datosCrudos);
if (!resultado.success) {
// rechaza: nunca dejes pasar lo que no encaja
}
// a partir de aqui, resultado.data es un valor tipado y de confianza
El lema parsea, no valides marca una diferencia profunda. Validar deja el dato crudo intacto y solo pregunta si cumple, confiando en que quien lo use recuerde que ya se comprobó. Parsear devuelve un valor nuevo, ya tipado, del que el sistema de tipos garantiza la forma de ahí en adelante: la comprobación y su resultado viajan juntos, y es imposible olvidar que se hizo. Aplícalo en el borde exacto donde el dato entra, para que el interior de tu código trabaje siempre con valores que ya pasaron la aduana.
La superficie de ataque de SSR
Un sitio estático se hornea y se sirve como ficheros: su superficie es mínima, porque no ejecuta lógica por visitante. En cuanto añades render bajo demanda, aparece una superficie nueva —código tuyo que corre en cada petición, con datos que el visitante controla— y con ella riesgos que lo estático no tenía. Vale la pena tenerlos mapeados.
// src/pages/perfil/[id].ts
import type { APIRoute } from 'astro';
export const GET: APIRoute = async ({ params }) => {
const id = Number(params.id);
if (!Number.isInteger(id) || id <= 0) {
return new Response('Parametro invalido', { status: 400 });
}
// usa consultas parametrizadas: nunca interpoles el id crudo en SQL
return new Response(/* ... */);
};
El mapa de la superficie SSR: los parámetros de ruta y la query son input externo, valídalos; las consultas a la base de datos han de ser parametrizadas, jamás cadenas concatenadas, o vuelves a la inyección —ahora SQL— que ya combatiste en el HTML; una URL que tu servidor recibe y luego pide puede convertirte en tránsito de un SSRF hacia servicios internos, así que restringe a dónde puede apuntar; los errores no deben devolver trazas ni detalles internos al cliente; y cualquier endpoint que muta necesita, como vimos, procedencia y autorización. La regla que lo unifica es la de siempre: no confíes en nada que venga del otro lado, y trata cada punto de entrada como una frontera que hay que sellar.
La superficie de SSR no es solo qué datos entran, sino cuántas veces. Un endpoint que consulta la base de datos, manda un correo o llama a una API de pago corre código costoso en cada petición, y sin freno es un blanco de abuso: fuerza bruta contra un login, agotamiento de un servicio de terceros, denegación de servicio por coste. La contramedida es limitar el ritmo —por IP, por sesión, por clave— y encarecer las operaciones sensibles con retardos o pruebas de trabajo. Lo estático no tenía este flanco porque no ejecutaba nada por visitante; en cuanto sirves bajo demanda, el volumen pasa a ser parte de tu modelo de amenazas.
flowchart TD
IN[input externo body params query] --> ZOD[safeParse con un esquema zod]
ZOD --> OK{encaja el esquema}
OK -->|si| DATA[valor tipado de confianza]
OK -->|no| REJ[rechaza con 400]
DATA --> USE[usalo en consultas parametrizadas]
style DATA fill:#a6e3a1,color:#11111b
style REJ fill:#f38ba8,color:#11111bLa forma más común de filtrar un secreto no es un bundle mal armado, es un git commit. Una clave que entra al historial de control de versiones se considera comprometida aunque la borres después, porque el historial la conserva y los repositorios se clonan. La disciplina: los secretos van en variables de entorno y en el gestor de secretos de tu plataforma, nunca en el código; el fichero de entorno local está en el .gitignore desde el primer día; y si una clave se filtró, no se borra —se rota, se invalida y se emite una nueva—. Prevén el escape con un escáner de secretos en el pre-commit, que atrapa la clave antes de que cruce a un lugar del que ya no puede volver.
En desarrollo quieres trazas detalladas; en producción son un regalo para el atacante. Un error que devuelve la consulta SQL que falló, la ruta absoluta de un fichero o la versión de una librería le entrega al que sondea justo el mapa que necesita. En producción, responde al cliente con un mensaje genérico y un código de estado, y manda el detalle a tus registros, no a la respuesta. La información sobre cómo está construido tu sistema es, ella misma, un activo que hay que proteger: cada dato interno que se filtra en un error acorta el camino del siguiente ataque.
Cadena de suministro
Audita el arbol con audit, fija versiones con el lockfile y minimiza dependencias. El codigo ajeno es confianza prestada.
astro:env
Declara contexto y acceso de cada variable. El secreto vive en el servidor y el build falla antes de filtrarlo.
Parsea, no valides
Zod convierte el input crudo en un valor tipado en el borde. La comprobacion y su resultado viajan juntos.
Superficie SSR
Cada peticion corre codigo tuyo con datos del visitante. Valida params, parametriza consultas, no filtres errores.
Conviene cerrar el nivel con la idea que lo ordena todo, porque es la que más cuesta interiorizar y la que más importa: la seguridad no es una característica que se añade, es una propiedad emergente del sistema completo, y como toda propiedad de un sistema, la determina su punto más frágil y no su punto más fuerte. Puedes escapar cada expresión, endurecer cada cabecera, cifrar cada canal, y perderlo todo por una dependencia comprometida que nunca auditaste, un secreto en un commit viejo, un parámetro que no validaste en un endpoint que creías menor. El atacante no ataca tu mejor defensa: busca la que olvidaste, porque el sistema entero cede por donde cede su eslabón más débil. De ahí se siguen los principios que esta lección repite bajo distintas formas y que conviene ver como uno solo. Minimizar la superficie: cada dependencia, cada endpoint, cada API del navegador encendida, cada dato que sale, es una puerta más que vigilar; la puerta más segura es la que no existe, y por eso reducir es, en seguridad, la forma más pura de defender. Desconfiar del borde: todo lo que cruza desde fuera —input, código de terceros, respuestas ajenas— es hostil hasta probar lo contrario, y la confianza se concede en un punto explícito, la aduana, no se presupone en el interior. Defensa en profundidad: ninguna capa es infalible, así que se apilan de modo que el fallo de una lo ataje la siguiente —el escape y encima la CSP, checkOrigin y encima SameSite, la validación y encima las consultas parametrizadas—. Y bajo todos, el mínimo privilegio: cada parte recibe solo el permiso que su función exige y ni una pizca más, para que comprometerla comprometa lo menos posible. Estos principios no son una lista de tareas que se completa; son una disposición, una forma de mirar cada línea preguntando qué confía en qué y qué pasaría si eso fallara. Un sistema no es seguro porque su autor conociera muchas vulnerabilidades, sino porque las trató todas como manifestaciones de las mismas pocas ideas —los datos no son código, la autoridad no debe flotar, el borde no es de fiar, el privilegio se concede a cuentagotas— y las aplicó con la terquedad de un hábito, en el eslabón brillante y en el oscuro por igual. La seguridad seria no es un estado que se alcanza y se archiva: es una vigilancia que no termina, porque el sistema cambia, las dependencias envejecen y el atacante solo necesita acertar una vez donde tú tienes que acertar siempre.
- Corre un
auditsobre tu proyecto, interpreta lo que reporta y actualiza al menos una dependencia con vulnerabilidad conocida, confirmando que ellockfilerefleja el cambio. - Declara un secreto de servidor y una variable pública con
astro:env, importa el secreto desdeastro:env/servere intenta usarlo en cliente para ver el build fallar antes de filtrarlo. - Valida el cuerpo de un endpoint con un esquema de Zod usando
safeParse, rechaza lo que no encaje con un400, y comprueba que a partir de ahí trabajas con un valor tipado. - Repasa un endpoint SSR y anota su superficie: qué params valida, si sus consultas son parametrizadas, y si un error de producción podría filtrar detalles internos al cliente.