Fingerprinting por profundidad de pila: recursion
Cómo la profundidad de pila JavaScript y los límites de recursión varían según navegador y plataforma para crear huellas digitales, y cómo controlar el comportamiento de la pila.
Prefieres la documentación del producto mantenida?
Este artículo tiene una página equivalente en el centro de documentación. Usa los docs para el flujo canónico, las flags actuales y la referencia duradera.
Introducción
Cada motor JavaScript tiene un límite en la profundidad de recursión de funciones. Cuando una función se llama a sí misma demasiadas veces, el motor lanza un error "Maximum call stack size exceeded". Este límite no está estandarizado. Varía entre navegadores, versiones de navegadores, sistemas operativos e incluso entre el hilo principal y Web Workers en el mismo navegador. Los sistemas de rastreo explotan esta variación midiendo exactamente cuántas llamadas recursivas son posibles antes de que la pila se desborde. El número resultante, la profundidad máxima de recursión, se convierte en una señal de huella digital. Debido a que este valor depende de los internos de bajo nivel del motor y la asignación de pila específica de la plataforma, es difícil de controlar desde JavaScript. Este artículo explica cómo funciona la huella digital por profundidad de pila y cómo BotBrowser proporciona un control preciso a través del flag --bot-stack-seed.
Impacto en la privacidad
La huella digital por profundidad de pila es una de las técnicas de huella digital más oscuras, pero proporciona entropía útil precisamente porque la mayoría de las herramientas de privacidad no la abordan. La investigación de grupos académicos que estudian la huella digital del navegador ha demostrado que los límites de recursión pueden distinguir entre versiones de navegadores, sistemas operativos e incluso builds de 32 bits vs. 64 bits del mismo navegador.
La técnica es efectiva porque:
- Varía entre navegadores. Chrome, Firefox y Safari asignan diferentes tamaños de pila predeterminados, produciendo diferentes profundidades máximas de recursión.
- Varía entre plataformas. La misma versión del navegador en Windows, macOS y Linux puede reportar profundidades diferentes debido a diferencias en la asignación de pila a nivel del SO.
- Varía entre contextos. El hilo principal, workers dedicados, workers compartidos y módulos WASM tienen diferentes límites de pila dentro del mismo navegador.
- Es estable. En la misma combinación navegador/SO/hardware, el límite de recursión es consistente entre sesiones.
Un estudio del equipo del navegador Brave señaló que la profundidad de pila proporciona aproximadamente 2-3 bits de entropía. Aunque esto es modesto, es valioso en huellas digitales compuestas porque se correlaciona con internos del motor del navegador que otras señales no capturan. Una discrepancia entre el user agent reportado y la profundidad de pila observada es una señal de inconsistencia fuerte.
Contexto técnico
Cómo funciona la profundidad de pila en JavaScript
Cuando se llama a una función JavaScript, el motor empuja un nuevo marco en la pila de llamadas. Cada marco contiene las variables locales de la función, los parámetros y la dirección de retorno. La pila de llamadas ocupa una región de memoria de tamaño fijo. Cuando la pila está llena, el motor lanza un RangeError.
La profundidad máxima depende de:
- Asignación del tamaño de pila. El SO asigna una cierta cantidad de memoria para la pila. Linux por defecto usa 8 MB (configurable vía
ulimit -s), macOS asigna 8 MB para el hilo principal y 512 KB para hilos secundarios, y Windows por defecto usa 1 MB. - Tamaño del marco. Cada marco de llamada tiene un tamaño diferente dependiendo del número de variables locales y la contabilidad interna del motor. Una función sin variables locales usa menos espacio de pila por marco que una con muchas.
- Optimizaciones del motor. V8 (Chrome), SpiderMonkey (Firefox) y JavaScriptCore (Safari) implementan la pila de llamadas de manera diferente, con diferentes tamaños de encabezado, requisitos de alineación y pasadas de optimización.
- Contexto de ejecución. El hilo principal típicamente tiene una pila más grande que los Web Workers. Los módulos WASM pueden tener su propia configuración de pila.
Medir la profundidad de pila
Un script de rastreo mide la profundidad de pila contando llamadas recursivas hasta que ocurre una excepción:
function measureDepth() {
let depth = 0;
function recurse() {
depth++;
recurse();
}
try {
recurse();
} catch (e) {
return depth;
}
}
El resultado depende del tamaño del marco de la función. Una función mínima (sin variables locales, sin argumentos) produce un conteo más alto que una función con muchas variables, pero ambos valores son estables para una combinación navegador/SO dada.
Variación entre contextos
Las mediciones de profundidad de pila difieren entre contextos de ejecución:
- Hilo principal típicamente tiene la pila más grande (a menudo 8 MB o más).
- Workers dedicados tienen pilas más pequeñas (a menudo 1 MB o 512 KB).
- Módulos WASM pueden tener sus propios límites de pila separados de JavaScript.
Estas diferencias son consistentes dentro de una combinación navegador/SO pero varían entre combinaciones. La proporción entre la profundidad del hilo principal y la profundidad del worker es en sí misma una señal distintiva.
Por qué fallan las protecciones existentes
La profundidad de pila está determinada por la gestión interna de pila del motor JavaScript. No puede modificarse desde JavaScript porque:
- La excepción es lanzada por el motor, no por código JavaScript.
- El tamaño de la pila es asignado por el SO cuando se crea el hilo.
- Interceptar el RangeError no cambia la profundidad a la que ocurre.
- Las extensiones de navegador se ejecutan en el mismo motor y no tienen acceso a la configuración de pila.
Enfoques comunes de protección y sus limitaciones
Esencialmente no existen protecciones a nivel de JavaScript para la huella digital por profundidad de pila. El valor está determinado por el motor y el SO, y ninguna extensión o script puede cambiarlo.
La suplantación del user agent cambia el navegador reportado pero no cambia el comportamiento real del motor. Afirmar ser Firefox mientras se ejecuta en Chrome no cambia la profundidad de pila específica de Chrome, creando una inconsistencia detectable.
Las máquinas virtuales pueden cambiar la profundidad de pila (debido a diferentes configuraciones del SO) pero introducen otras señales de huella digital.
Compilar builds personalizados del navegador con tamaños de pila modificados es posible pero impráctico para la mayoría de los usuarios. También requiere coincidir con todas las demás señales a nivel del motor para mantener la consistencia.
Aleatorizar la medición atrapando excepciones antes de tiempo no ayuda. Un script de rastreo puede llamar a la función de medición múltiples veces y tomar el máximo, o usar diferentes formas de función para triangular el límite real.
El único enfoque efectivo es controlar el comportamiento real de la pila del motor, lo cual requiere modificación a nivel del navegador.
El enfoque a nivel de motor de BotBrowser
BotBrowser proporciona control directo sobre el comportamiento de la profundidad de pila JavaScript a través del flag --bot-stack-seed.
Tres modos de control
El flag --bot-stack-seed acepta tres tipos de valores:
profile - Coincide con la profundidad de pila exacta del perfil. El límite de recursión coincide con lo que produciría el dispositivo perfilado. Esta es la opción más precisa para mantener una identidad de dispositivo consistente.
real - Usa la profundidad de pila nativa. No se aplica modificación. El límite de recursión refleja tu hardware y configuración de SO reales.
Semilla entera (1-UINT32_MAX) - Genera una variación de profundidad de pila determinista por sesión. Cada semilla produce un valor de profundidad diferente pero estable. Esto es útil cuando necesitas sesiones distintas que cada una tenga una profundidad de pila consistente pero que difieran entre sí.
Cobertura multi-contexto
El control de profundidad de pila de BotBrowser se aplica en todos los contextos de ejecución JavaScript:
- Los límites de recursión del hilo principal están controlados.
- Los Web Workers (dedicados y compartidos) tienen sus profundidades de pila controladas independientemente pero de manera consistente con el perfil.
- El comportamiento de la pila de módulos WASM también está controlado.
La proporción entre las profundidades del hilo principal y el worker coincide con lo que exhibiría el dispositivo perfilado, asegurando que las mediciones entre contextos produzcan resultados consistentes.
Consistencia con otras señales
La profundidad de pila controlada se alinea con el user agent, la plataforma y la versión del navegador del perfil. Si el perfil especifica Chrome 120 en Windows 10, la profundidad de pila coincide con lo que Chrome 120 en Windows 10 realmente produce. No hay discrepancia entre la afirmación del user agent y el comportamiento observable del motor.
Variación basada en semilla
Cuando se usa una semilla entera, la profundidad de pila se deriva determinísticamente del valor de la semilla. La misma semilla siempre produce la misma profundidad. Diferentes semillas producen diferentes profundidades dentro de un rango realista para la combinación navegador/SO perfilada. Esto te permite crear múltiples identidades distintas que cada una tenga una profundidad de pila plausible.
Configuración y uso
Profundidad de pila del perfil
chrome --bot-profile="/path/to/profile.enc" \
--bot-stack-seed=profile \
--user-data-dir="$(mktemp -d)"
Variación basada en semilla
# Profundidad de pila determinista desde semilla
chrome --bot-profile="/path/to/profile.enc" \
--bot-stack-seed=42 \
--user-data-dir="$(mktemp -d)"
# Profundidad diferente con diferente semilla
chrome --bot-profile="/path/to/profile.enc" \
--bot-stack-seed=43 \
--user-data-dir="$(mktemp -d)"
Profundidad de pila nativa
chrome --bot-profile="/path/to/profile.enc" \
--bot-stack-seed=real \
--user-data-dir="$(mktemp -d)"
Combinado con otros flags deterministas
chrome --bot-profile="/path/to/profile.enc" \
--bot-stack-seed=profile \
--bot-noise-seed=42 \
--bot-time-seed=42 \
--user-data-dir="$(mktemp -d)"
Integración con Playwright
const { chromium } = require('playwright');
const browser = await chromium.launch({
executablePath: '/path/to/botbrowser/chrome',
args: [
'--bot-profile=/path/to/profile.enc',
'--bot-stack-seed=profile'
]
});
const page = await browser.newPage();
await page.goto('https://example.com');
// Medir profundidad de pila
const depth = await page.evaluate(() => {
let d = 0;
function r() { d++; r(); }
try { r(); } catch(e) {}
return d;
});
console.log(`Stack depth: ${depth}`);
Integración con Puppeteer
const puppeteer = require('puppeteer');
const browser = await puppeteer.launch({
executablePath: '/path/to/botbrowser/chrome',
defaultViewport: null,
args: [
'--bot-profile=/path/to/profile.enc',
'--bot-stack-seed=profile'
]
});
const page = await browser.newPage();
await page.goto('https://example.com');
Verificación
Medición de profundidad. Ejecuta una función recursiva en la consola del navegador y registra la profundidad. Compárala entre sesiones con el mismo perfil y semilla de pila. La profundidad debería ser idéntica.
Medición de profundidad del worker. Ejecuta la misma prueba de recursión en un Web Worker. La profundidad debería ser diferente del hilo principal (los workers tienen pilas más pequeñas) pero consistente entre sesiones.
Verificación entre máquinas. Ejecuta la misma prueba en una máquina diferente con el mismo perfil y semilla. La profundidad de pila debería coincidir.
Variación de semilla. Cambia la semilla y verifica que la profundidad cambie. Esto confirma que la semilla está controlando activamente el comportamiento.
Mejores prácticas
- Usa
--bot-stack-seed=profilepara máxima precisión. Esto coincide con la profundidad de pila exacta del dispositivo perfilado, asegurando consistencia con el user agent y la plataforma. - Combina con
--bot-noise-seedy--bot-time-seed. La profundidad de pila, el ruido de renderizado y el comportamiento de temporización son todos parte de la huella digital general del navegador. Controla los tres para una protección completa. - No establezcas valores de profundidad irrealistas. Si usas una semilla, la profundidad generada cae dentro del rango realista para el navegador perfilado. La especificación manual de valores extremos podría ser inconsistente con otras señales.
- Prueba en contextos de worker. La profundidad de pila varía entre el hilo principal y los workers. Verifica ambos.
- Entiende el alcance. La profundidad de pila es una señal entre muchas. Contribuye a la huella digital general pero no es suficiente por sí sola. Siempre usa un perfil completo.
Preguntas frecuentes
P: ¿Cuánta entropía proporciona la profundidad de pila? R: Aproximadamente 2-3 bits por sí sola. El valor distingue entre familias de navegadores, tipos de plataforma y a veces versiones específicas. Su valor aumenta en huellas digitales compuestas donde otras señales han sido controladas.
P: ¿La profundidad de pila cambia con las actualizaciones del navegador? R: A veces. Los cambios mayores en el motor V8 pueden alterar el tamaño del marco o la asignación de pila, lo que cambia la profundidad máxima de recursión. Esta es una razón por la que los perfiles deberían actualizarse para coincidir con las versiones actuales del navegador.
P: ¿Puede un sitio web medir mi profundidad de pila sin mi conocimiento? R: Sí. La medición es una simple llamada a función recursiva que se ejecuta en milisegundos y no produce ningún efecto visible. No se necesitan permisos.
P: ¿La profundidad de pila afecta la ejecución normal de JavaScript? R: La profundidad de pila controlada solo aplica al límite medido. El código normal de aplicaciones web raramente se acerca al límite de recursión. Las aplicaciones web típicas usan mucho menos profundidad de pila que el máximo.
P: ¿La pila WASM se controla por separado?
R: Sí. Los módulos WASM tienen su propia configuración de pila. El flag --bot-stack-seed de BotBrowser controla el comportamiento de la pila WASM junto con el de JavaScript.
P: ¿Qué pasa si necesito ejecutar algoritmos profundamente recursivos?
R: El control de profundidad de pila establece el límite medido, no un tope artificial. Usar --bot-stack-seed=real te da la profundidad de pila nativa si tu aplicación necesita recursión profunda.
P: ¿La profundidad de pila difiere entre modo headless y con interfaz gráfica? R: En Chromium estándar, la profundidad de pila es la misma independientemente del modo de visualización. BotBrowser mantiene esta consistencia con sus valores controlados.
Resumen
La profundidad de pila JavaScript es una señal de huella digital oscura pero efectiva que varía entre navegadores, plataformas y contextos de ejecución. Debido a que está determinada por los internos del motor y la asignación de pila a nivel del SO, no puede controlarse desde JavaScript. El flag --bot-stack-seed de BotBrowser proporciona control directo sobre los límites de recursión a nivel del motor, con opciones para coincidir con la profundidad exacta del perfil, usar la profundidad nativa o generar variación determinista desde una semilla. Combinado con semillas de ruido y temporización, el control de profundidad de pila completa el enfoque integral de BotBrowser para el comportamiento determinista del navegador.
Para temas relacionados, consulta Qué es la huella digital del navegador, Comportamiento determinista del navegador, Protección de propiedades del navegador y Huella digital de temporización de rendimiento.
Artículos Relacionados
Lleva BotBrowser de la investigación a producción
Usa estas guías para entender el modelo y después avanzar hacia validación multiplataforma, contextos aislados y despliegue de navegador preparado para escalar.