Saltar al contenido

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.

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.

HTTP
pnpm audit --audit-level=highpnpm 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—.

TypeScript
// astro.config.mjsimport { 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.

TypeScript
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.

TypeScript
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.

TypeScript
// src/pages/perfil/[id].tsimport 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.