Seguridad en JavaScript: riesgos y controles | Nicolás Garzón
fronteras de confianza
Texto
Copiar entrada no confiable
↓
validar estructura y dominio
↓
representación interna segura
↓
encoding según el destino
↓
efecto autorizado
Inputs y formularios.
Query params y fragments.
Respuestas de APIs.
localStorage e IndexedDB.
postMessage.
Archivos.
Cookies legibles.
Datos de terceros.
Configuración de build.
No confíes en un valor solo porque lo generó una versión anterior de tu propia aplicación.
JavaScript
Copiar const product = parseProduct ( await response. json ( ) ) ; Comprueba forma, tipo e invariantes antes de usar datos en el núcleo.
La validación no vuelve seguro insertar texto en cualquier destino. Después debes aplicar la codificación adecuada al contexto.
Un mismo valor requiere reglas diferentes para:
Texto HTML.
Atributo HTML.
URL.
JavaScript inline.
CSS.
SQL.
Shell.
No existe una función universal escapeEverything.
Cross-Site Scripting ocurre cuando datos no confiables terminan ejecutándose como código en el origen de la aplicación.
JavaScript
Copiar element. innerHTML = userInput; JavaScript
Copiar element. textContent = userInput; Si necesitas HTML, utiliza una sanitización mantenida y una política clara de elementos y atributos permitidos.
JavaScript
Copiar location. search;
location. hash;
localStorage;
messageEvent. data; Sinks peligrosos incluyen:
JavaScript
Copiar innerHTML;
outerHTML;
insertAdjacentHTML;
eval;
new Function ;
setTimeout ( string) ; La vulnerabilidad puede existir completamente en frontend sin que el servidor refleje el texto.
Una política CSP puede exigir objetos TrustedHTML para sinks sensibles.
JavaScript
Copiar Content- Security- Policy: require- trusted- types- for 'script' Trusted Types ayuda a centralizar creación de HTML confiable, pero no convierte automáticamente entrada insegura en segura.
JavaScript
Copiar Content- Security- Policy: default - src 'self' ; script- src 'self' ; object- src 'none' CSP limita fuentes y ciertos comportamientos. Una política fuerte evita scripts inline inseguros mediante nonces o hashes.
Es defensa en profundidad, no sustituto de evitar inyección.
JavaScript
Copiar eval ( userInput) ;
new Function ( userInput) ( ) ; Ejecutar entrada como código permite control total dentro de los permisos de la página.
Para datos utiliza JSON. Para fórmulas o reglas, diseña un lenguaje limitado y parser propio o una librería especializada.
JavaScript
Copiar Object. assign ( target, untrustedObject) ; Claves como __proto__, constructor o prototype pueden alterar comportamiento en combinaciones vulnerables.
Validar claves permitidas.
Usar objetos con prototipo null cuando corresponde.
No hacer deep merge genérico de entrada externa.
Mantener librerías actualizadas.
Usar Object.hasOwn.
JavaScript
Copiar if ( user. role === "admin" ) {
showAdminButton ( ) ;
} Esto mejora UI, pero no protege la operación. El servidor debe verificar identidad y permiso en cada acción sensible.
Ocultar un botón no es control de acceso.
Cualquier token accesible al JavaScript de la página puede ser robado por XSS.
Cookies HttpOnly reducen lectura directa, pero necesitan:
Secure.
SameSite apropiado.
CSRF protection.
Expiración e invalidación.
Protección XSS igualmente.
No guardes claves privadas o secretos permanentes en frontend.
Cuando el navegador envía credenciales automáticamente, un sitio externo puede intentar provocar acciones.
SameSite.
Token CSRF.
Origin checks.
No usar GET para efectos.
Confirmación o reautenticación para acciones críticas.
JavaScript
Copiar location. href = params. get ( "next" ) ; Un atacante puede crear enlaces que envían a una página maliciosa.
JavaScript
Copiar function parseInternalRedirect ( value ) {
const url = new URL ( value, location. origin) ;
if ( url. origin !== location. origin) {
throw new Error ( "External redirect is not allowed" ) ;
}
return url;
} JavaScript
Copiar window. addEventListener ( "message" , ( event ) => {
if ( event. origin !== TRUSTED_ORIGIN ) return ;
const message = parseMessage ( event. data) ;
} ) ; Al enviar datos sensibles, especifica targetOrigin; no utilices *.
Un enlace con target="_blank" puede obtener una referencia a la ventana que lo abrió en ciertos escenarios.
HTML
Copiar < a
href = " https://external.example"
target = " _blank"
rel = " noopener noreferrer"
> Navegadores modernos aplican algunas protecciones por defecto, pero expresar la intención sigue siendo claro.
Código ejecutable.
Dependencias transitivas.
Riesgo de supply chain.
Mantenimiento y licencias.
Lockfile revisado.
Actualizaciones controladas.
Auditoría de paquetes.
Mínimos permisos en CI.
No ejecutar scripts desconocidos sin revisar.
Evitar dependencias innecesarias.
JavaScript
Copiar const apiSecret = import . meta. env. API_SECRET ; Si el valor se incluye en un bundle frontend, el usuario puede obtenerlo. El prefijo “secret” no cambia eso.
Mantén secretos en servidores o entornos no entregados al cliente.
Una RegExp vulnerable puede consumir CPU excesiva con entrada diseñada.
JavaScript
Copiar / ^ ( a+ ) + $ / ; Limita tamaño, revisa backtracking y evita patrones ambiguos en datos no confiables.
Un payload muy grande o profundo puede agotar memoria o bloquear parsing.
Content-Length.
Límites del servidor.
Profundidad.
Cantidad de elementos.
Tiempo de procesamiento.
No implementes cifrado, hashing de contraseñas o generación de tokens con Math.random.
Utiliza APIs y librerías revisadas:
JavaScript
Copiar crypto. getRandomValues ( array) ;
crypto. subtle; Web Crypto tiene contratos complejos; elegir algoritmo, parámetros y gestión de claves sigue requiriendo diseño especializado.
Tokens.
Passwords.
Datos personales completos.
Stack traces internos al usuario.
URLs firmadas.
Registra contexto mínimo útil con controles de acceso y retención.
JavaScript
Copiar function renderComment ( comment ) {
const article = document. createElement ( "article" ) ;
const author = document. createElement ( "strong" ) ;
const body = document. createElement ( "p" ) ;
author. textContent = comment. authorName;
body. textContent = comment. body;
article. append ( author, body) ;
return article;
} Los datos no se interpretan como markup.
¿De dónde viene el dato?
¿Fue validado?
¿En qué contexto se insertará?
¿Quién autoriza la acción?
¿Puede incluir secretos?
¿Puede ser enorme?
¿Qué dependencias lo procesan?
¿Existe un cleanup o timeout?
¿Qué información se registra?
¿Qué defensa adicional aplica el navegador?
Confiar en validación del frontend para autorización.
Insertar texto con innerHTML.
Usar eval para procesar datos.
Guardar secretos en bundles.
Confiar en localStorage como bóveda.
Aceptar cualquier origin en postMessage o CORS.
Hacer merges profundos de objetos externos.
Implementar criptografía manual.
Exponer stack traces y tokens.
Añadir dependencias sin evaluar supply chain.
Seguridad empieza en fronteras de confianza.
Validar y escapar resuelven problemas distintos.
textContent evita interpretar texto como HTML.
La autorización debe verificarse en el servidor.
HttpOnly reduce lectura de cookies, pero no elimina XSS.
CORS, CSRF y CSP tienen responsabilidades diferentes.
El frontend no puede contener secretos reales.
Las dependencias también son código confiado.
Límites de tamaño y tiempo son controles de seguridad.
¿Por qué validar que comment.body es string no basta antes de asignarlo a innerHTML?
Respuesta Porque un string puede contener markup y scripts peligrosos. La validación del tipo no aplica la codificación o sanitización requerida por el contexto HTML.
Testing de JavaScript explica cómo verificar contratos, límites, efectos y asincronía.