Verifica la integridad del CDN con sha384sum en navegadores
Garantizar la integridad de los recursos que se sirven desde una Content Delivery Network (CDN) es
fundamental para proteger a tus usuarios y tu reputación. Subresource Integrity (SRI) permite que
los navegadores verifiquen que un archivo descargado coincide con un hash criptográfico esperado, lo
que bloquea de forma efectiva los recursos comprometidos. En este DevTip veremos por qué
sha384sum es el punto ideal para los hashes de SRI, cómo generar e insertar esos hashes y cómo
automatizar todo el flujo de trabajo desde la línea de comandos hasta tu pipeline de CI/CD.
Comprende Subresource Integrity (SRI)
Un navegador compatible con SRI sigue estos pasos cuando encuentra el atributo integrity:
- Descarga el script o la hoja de estilos a la que hace referencia el elemento.
- Calcula su hash sobre la marcha.
- Compara el resultado con el hash escrito directamente en el HTML.
- Aborta la ejecución si los valores difieren.
El resultado es simple pero potente: si alguien manipula un script de terceros, el navegador se niega a ejecutarlo.
¿Por qué elegir SHA-384 para SRI?
SRI admite SHA-256, SHA-384 y SHA-512. En este artículo usamos SHA-384: tiene amplia compatibilidad y produce un digest más corto que SHA-512. La velocidad del hashing depende de la implementación y del hardware; elige un algoritmo compatible en lugar de guiarte por una clasificación general de velocidad.
Genera un hash en la línea de comandos
sha384sum imprime un digest en hexadecimal, pero SRI necesita base64. Usa openssl (fácilmente
disponible en macOS y Linux) para obtener la salida correcta:
cat your-file.js | openssl dgst -sha384 -binary | openssl base64 -A
Para crear el valor SRI completo de una sola vez, anteponle sha384-:
echo "sha384-$(cat your-file.js | openssl dgst -sha384 -binary | openssl base64 -A)"
Inserta SRI en tu HTML o JSX
Reemplaza cada hash de ejemplo de abajo con el digest de los bytes de la versión confiable de ese
archivo exacto. Los navegadores usan el algoritmo compatible más fuerte cuando se proporcionan
varios algoritmos; no recurren a un hash más débil tras una discrepancia. En JSX, escribe el
atributo crossOrigin.
<!-- Single hash example -->
<script
src="https://example.com/js/library.js"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
crossorigin="anonymous"
></script>
<!-- Multiple algorithms; replace these example digests for this stylesheet -->
<link
rel="stylesheet"
href="https://example.com/css/styles.css"
integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC sha512-/OeNzF1PFA/xbzX92DyMRPz3opJ4nVDJ+EYhXpqwOFpq1DMLulFbN5OPUXmHkXmNovSH7BwpUQX/xFG0QZgTcA=="
crossorigin="anonymous"
/>
El atributo crossorigin="anonymous" es obligatorio cuando el archivo proviene de un origen distinto.
El CDN también debe permitir el origen solicitante mediante CORS. Sin esas condiciones, el navegador
bloquea el recurso de origen cruzado en lugar de omitir en silencio la verificación de integridad.
Genera hashes en el navegador con la Web Crypto API
Cuando necesites validar o calcular hashes en tiempo de ejecución (piensa en extensiones de
navegador, páginas de pruebas de seguridad o paneles de control), llama a crypto.subtle.digest:
async function generateSRIHash(url) {
const response = await fetch(url, {
cache: 'no-store',
signal: AbortSignal.timeout(10_000),
})
if (!response.ok) throw new Error(`Unable to fetch resource: HTTP ${response.status}`)
const buffer = await response.arrayBuffer()
const hashBuffer = await crypto.subtle.digest('SHA-384', buffer)
// Convert the ArrayBuffer to a base64 string
const hashArray = Array.from(new Uint8Array(hashBuffer))
const binaryString = String.fromCharCode.apply(null, hashArray)
const base64Hash = btoa(binaryString)
return `sha384-${base64Hash}`
}
// Demo usage
generateSRIHash('https://cdn.example.com/library.min.js').then(console.log).catch(console.error)
crypto.subtle está disponible en todos los navegadores evergreen actuales, pero solo en orígenes seguros
(https:// o http://localhost durante el desarrollo).
Los ejemplos en tiempo de ejecución usan AbortSignal.timeout(), disponible en los navegadores evergreen
actuales, para limitar cada solicitud a diez segundos de tiempo activo. Esto evita que una descarga
estancada o una alerta bloqueen las comprobaciones de monitoreo posteriores; ajusta el tiempo de
espera según los tamaños de recurso que esperes.
Esta función calcula un digest, no un veredicto sobre la autenticidad. Compáralo con el digest de
una versión confiable. Generar un nuevo hash esperado desde el mismo CDN comprometido sería confiar
en el reemplazo del atacante. Descargar una URL y calcular su hash tampoco demuestra qué bytes
ejecutó previamente una solicitud de script independiente; el atributo integrity del elemento impone esa comprobación.
Compatibilidad con navegadores
Subresource Integrity es compatible con:
- Chrome 45+
- Firefox 43+
- Safari 11.1+
- Edge 17+
Todos los anteriores también incluyen la Web Crypto API. Los navegadores más antiguos simplemente
ignoran el atributo integrity y cargan el archivo con normalidad.
La Web Crypto API que se usa para generar hashes requiere un contexto seguro (HTTPS).
Automatiza SRI en tu pipeline de CI/CD
Un paso de compilación garantiza que los hashes se mantengan sincronizados con tus bundles. Abajo
hay un job mínimo de GitHub Actions que compila los recursos y los hashes desde el mismo checkout.
Instala globby como dependencia de desarrollo, confirma el archivo de bloqueo y usa Node.js 22 o
una versión posterior. Tu compilación debe generar dist/ antes del paso de hashing; después
renderiza las plantillas con el manifiesto generado y despliega ambos juntos:
name: build
on: [push]
permissions:
contents: read
jobs:
sri:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Install dependencies
run: npm ci
- name: Build assets
run: npm run build
- name: Generate SRI hashes
run: |
node scripts/generate-sri.mjs
- name: Save assets and hash manifest
uses: actions/upload-artifact@v4
with:
name: assets-with-integrity
path: |
dist/
sri-hashes.json
scripts/generate-sri.mjs:
import { createHash } from 'node:crypto'
import { readFileSync, writeFileSync } from 'node:fs'
import { globby } from 'globby'
function sri(file) {
const data = readFileSync(file)
const hash = createHash('sha384').update(data).digest('base64')
return `sha384-${hash}`
}
const files = await globby(['dist/**/*.js', 'dist/**/*.css'])
if (files.length === 0) throw new Error('No built assets found in dist/')
const map = Object.fromEntries(files.map((f) => [f, sri(f)]))
writeFileSync('sri-hashes.json', `${JSON.stringify(map, null, 2)}\n`)
console.table(map)
Tu plantilla de compilación (p. ej., Astro, Eleventy, Next.js) puede leer entonces el JSON e inyectar el hash correcto automáticamente.
Carga scripts de forma dinámica
Para code-splitting o interruptores de funcionalidades, a menudo creas elementos sobre la marcha. Asegúrate de aplicar también el hash de forma programática:
export async function loadScript(src, integrity) {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
Object.assign(script, {
src,
integrity,
crossOrigin: 'anonymous',
})
script.addEventListener('load', () => resolve())
script.addEventListener('error', () => reject(new Error(`Failed to load or verify ${src}`)))
document.head.append(script)
})
}
Monitorea los recursos del CDN en producción
Una pequeña clase auxiliar puede vigilar los archivos que cambian con frecuencia y avisar a tu stack
de observabilidad cuando el contenido descargado difiera de un hash esperado confiable. Esto es
monitoreo periódico, no una garantía en tiempo real. Define un endpoint /api/security-alerts autenticado y con
límite de tasa antes de usar este ejemplo, y no incluyas en las alertas URL de recursos que
contengan credenciales:
class SRIMonitor {
#entries = new Map()
#intervalId
#checking = false
constructor(interval = 5 * 60_000) {
this.interval = interval
}
add(url, expectedHash) {
this.#entries.set(url, expectedHash)
}
async #check(url, expected) {
const actual = await generateSRIHash(url)
if (actual !== expected) {
console.warn(`[SRI] Mismatch for ${url}`)
// Push to your SecOps webhook / Slack / PagerDuty …
const response = await fetch('/api/security-alerts', {
method: 'POST',
signal: AbortSignal.timeout(10_000),
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url, expected, actual }),
})
if (!response.ok) throw new Error(`Alert delivery failed: HTTP ${response.status}`)
}
}
async #poll() {
if (this.#checking) return
this.#checking = true
try {
for (const [url, hash] of this.#entries) {
try {
await this.#check(url, hash)
} catch (error) {
console.error('SRI monitor check failed:', error)
}
}
} finally {
this.#checking = false
}
}
start() {
if (this.#intervalId !== undefined) return
this.#intervalId = setInterval(() => {
void this.#poll()
}, this.interval)
}
stop() {
clearInterval(this.#intervalId)
this.#intervalId = undefined
}
}
Consideraciones de seguridad
- Sirve siempre los recursos con SRI habilitado a través de HTTPS.
- Incluye el atributo
crossorigin="anonymous"cuando el recurso esté en un dominio distinto. - Obtén los hashes esperados de una versión confiable o de tu propia compilación, de forma independiente del CDN.
- Ten en cuenta que SRI deja de funcionar si el recurso cambia: define una estrategia para actualizar los hashes, idealmente automatizada en tu pipeline de CI/CD.
- Combina SRI con una Content Security Policy (CSP) que permita solo las fuentes de scripts
necesarias y que autorice los scripts en línea con nonces o hashes en lugar de
'unsafe-inline'.
Soluciona problemas comunes
| Síntoma | Causa probable y solución |
|---|---|
| El script carga pero no se ejecuta | Discrepancia de hash → regenerar y volver a desplegar |
| Funciona en local, falla en PROD | La minificación del CDN cambia los bytes decodificados; la compresión HTTP habitual no cambia el digest |
Blocked by CORS policy | Falta el atributo crossorigin o el CDN carece de las cabeceras adecuadas (p. ej. Access-Control-Allow-Origin) |
| SRI se ignora por completo | Navegador no compatible, elemento no compatible o metadatos de integridad ausentes |
Implementa alternativas de respaldo
Si la verificación falla, puedes recurrir a un mirror confiable idéntico byte a byte o a una copia alojada por ti. Mantén habilitada la verificación de integridad en la alternativa de respaldo:
async function loadWithFallback(primary, backup, integrity) {
try {
await loadScript(primary, integrity)
} catch {
console.warn(`Primary failed, switching to ${backup}`)
await loadScript(backup, integrity)
}
}
Cómo usa Transloadit el hashing de archivos
En Transloadit ofrecemos el Robot 🤖 /file/hash, que admite varios algoritmos de hash, incluido SHA-384. Aunque este Robot puede usarse como parte de tu pipeline de procesamiento de medios, para generar SRI en concreto deberías usar los métodos descritos arriba. El Robot calcula los digests de los archivos dentro de las Assemblies; tu aplicación debe compararlos con valores esperados confiables para verificar la integridad.
Al adoptar SRI con sha384sum, y automatizarlo desde el desarrollo hasta el despliegue, añades una
importante capa adicional de defensa contra los ataques a la cadena de suministro sin sacrificar el rendimiento.
