Resumen
Señales derivadas de características de la plataforma web, capacidades CSS, fuentes y otras características del navegador.
1. Fingerprinting de Fuentes
Mecanismo
Las fuentes se enumeran mediante varias técnicas:
-
Medición CSS
@font-face: Crear texto, medir su anchura con una fuente conocida, luego intentar cargar una fuente del sistema. Si la anchura cambia, la fuente existe. -
Canvas
measureText: Enfoque similar usando canvas. -
document.fonts.check(): API moderna para verificar disponibilidad de fuentes.
Enumeración basada en Flash (heredada, aún usada): Flash puede listar todas las fuentes instaladas.
Ejemplo de Código
// Detección de fuentes basada en medición
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const text = 'mmmmmmmmmmlli'
function detectFont(font) {
ctx.font = `72px ${font}, monospace`
return ctx.measureText(text).width
}
// Comparar anchura con la línea base monospace para detectar
Entropía
La lista de fuentes varía ampliamente:
| Plataforma | Fuentes Típicas | Unicidad |
|---|---|---|
| Windows 11 stock | ~80 fuentes | Media |
| macOS Ventura+ | ~120 fuentes | Media |
| Linux (distro varía) | 10-500 fuentes | Muy Alta |
| Fuentes personalizadas instaladas | Cualquiera | Muy Alta |
Mitigación en el Proxy
// Anular medición de fuentes para devolver valores consistentes
const originalMeasureText = CanvasRenderingContext2D.prototype.measureText
CanvasRenderingContext2D.prototype.measureText = function(text) {
const result = originalMeasureText.call(this, text)
// Redondear anchura para reducir precisión de detección de fuentes
result.width = Math.round(result.width / 10) * 10
return result
}
Limitación: La detección de fuentes solo se reduce, no se elimina.
2. Soporte de Características del Navegador (25+ vectores binarios)
Mecanismo
Cada navegador soporta un subconjunto diferente de APIs web y características CSS. Colectivamente, esto crea un fingerprint de navegador+versión.
Características de Alta Entropía
| Característica | Chrome | Firefox | Safari |
|---|---|---|---|
| WebGL | Sí | Sí | Sí |
| WebGPU | Sí (flag) | No | No |
| WebUSB | Sí | No | No |
| Web Bluetooth | Sí | No | No |
| Web NFC | Android | No | No |
| Web Authentication | Sí | Sí | Sí |
| Web Assembly | Sí | Sí | Sí |
| Service Workers | Sí | Sí | Sí |
| Battery API | No | Sí | No |
| Speech Synthesis | Sí | Sí | Sí |
| GamePad API | Sí | Sí | Sí (parcial) |
| WebRTC | Sí | Sí | Sí |
| CSS Grid | Sí | Sí | Sí |
| CSS backdrop-filter | Sí | Sí | Sí |
| CSS Container Queries | Sí | Sí | Sí |
Entropía
Cada característica añade ~1 bit. Con ~100 características probadas, el máximo teórico es 100 bits, pero las características están altamente correlacionadas (entropía mínima más allá de la familia del navegador y la versión).
Entropía efectiva: ~8-12 bits (~256-4096 combinaciones).
Mitigación
No se puede hacer a nivel de proxy — estas son capacidades inherentes del navegador. La detección de características se ejecuta en JS antes de que cualquier script inyectado pueda ejecutarse (e incluso si se anula, el navegador aún reporta las capacidades reales en algún lugar).
3. Fingerprinting mediante Media Queries CSS
Mecanismo
Las @media queries CSS pueden obtener por fuerza bruta dimensiones de pantalla, DPI, profundidad de color y otras propiedades sin JS:
@media (width: 1920px) { body::after { content: '1920' } }
@media (height: 1080px) { body::after { content: '1080' } }
@media (resolution: 1.5dppx) { body::after { content: '1.5' } }
@media (prefers-color-scheme: dark) { ... }
@media (prefers-reduced-motion: no-preference) { ... }
@media (hover: hover) { ... }
@media (pointer: fine) { ... }
Desafío para el Proxy
CSS se ejecuta antes de cualquier inyección JS. El proxy puede:
- Analizar el CSS en el HTML de respuesta y modificar las media queries
- O confiar en que las sondas de fingerprinting solo con CSS necesitan exfiltrar datos (lo que necesita JS o una petición de red)
Mitigación real: Bloquear la ruta de exfiltración (JS o carga de imágenes) en lugar de falsificar valores CSS.
Lista de Media Queries
| Query | Valores | Información Filtrada |
|---|---|---|
| width | px | Ancho de pantalla |
| height | px | Alto de pantalla |
| aspect-ratio | ratio | Relación de pantalla |
| resolution | dppx | Densidad de píxeles |
| color | bits | Profundidad de color |
| color-gamut | srgb/p3/rec2020 | Espacio de color |
| prefers-color-scheme | light/dark | Tema del SO |
| prefers-reduced-motion | no-preference/reduce | Configuración del SO |
| prefers-contrast | no-preference/more/less | Configuración del SO |
| prefers-reduced-transparency | no-preference/reduce | Configuración del SO |
| hover | none/hover | Dispositivo de entrada |
| pointer | none/coarse/fine | Dispositivo de entrada |
| any-hover | none/hover | Cualquier entrada |
| any-pointer | none/coarse/fine | Cualquier entrada |
| display-mode | fullscreen/standalone | Modo PWA |
| prefers-color-gamut | srgb/p3 | Gama de pantalla |
| dynamic-range | standard/high | Soporte HDR |
| inverted-colors | none/inverted | Accesibilidad del SO |
| monochrome | bits | Pantalla monocromática |
| forced-colors | none/active | Accesibilidad |
| prefers-reduced-data | no-preference/reduce | Ahorro de datos |
4. Fingerprinting de ClientRects
Mecanismo
Element.getClientRects() devuelve valores ligeramente diferentes según el SO, navegador y renderizado de fuentes:
const span = document.createElement('span')
span.textContent = 'mmmmmmmmmmlli'
document.body.appendChild(span)
const rects = span.getClientRects()
// rects[0].width varía según SO/navegador/fuente
Mitigación
Element.prototype.getClientRects = function() {
const rects = Element.prototype.__proto__.getClientRects.call(this)
return Array.from(rects).map(r => ({
...r,
width: Math.round(r.width / 10) * 10,
height: Math.round(r.height / 10) * 10
}))
}
5. Fingerprint de Web Audio API
Ver audio-fingerprinting.md para detalles.
6. Fingerprinting de WebGPU
WebGPU proporciona información estructurada del dispositivo no disponible anteriormente:
const adapter = await navigator.gpu.requestAdapter()
const info = await adapter.requestAdapterInfo()
// { vendor: "nvidia", architecture: "ampere", device: "..." }
// Los límites del adapter exponen capacidades GPU específicas
const limits = adapter.limits
// maxTextureDimension2D, maxStorageBufferBindingSize, etc.
// Conjunto de características
adapter.features.has('texture-compression-bc')
adapter.features.has('shader-f16')
Datos Expuestos
| Campo | Ejemplo | Identificabilidad |
|---|---|---|
| vendor | "nvidia", "apple", "amd", "intel" | Baja (4-5 valores) |
| architecture | "ampere", "rdna2", "tigerlake" | Media |
| device | "integrated-graphics", "Apple M3" | Media |
| driver | "460.32.03" | Alta |
| backend | "Metal", "Vulkan", "D3D12" | Baja |
| type | "discrete-gpu", "integrated-gpu", "cpu" | Baja |
| memoryHeaps | tamaño y conteo | Alta |
| limits | 50+ valores de capacidad | Muy Alta |
Mitigación
// Bloquear información de WebGPU adapter
navigator.gpu.requestAdapter = async function() {
return null // o devolver un adapter mínimo falso
}
Costo: Bloquea WebGPU por completo, lo que rompe algunas aplicaciones.
7. Persistencia de Service Worker
Mecanismo
Los Service Workers pueden usarse para identificar usuarios entre sesiones:
// Detectar si un SW está instalado
navigator.serviceWorker.getRegistrations().then(regs => {
if (regs.length > 0) console.log('SW encontrado')
})
Mitigación
navigator.serviceWorker.getRegistrations =
async () => []
8. Fingerprint de Extensiones WebGL
WebGL expone una lista de extensiones soportadas que varía según GPU, controlador y navegador:
| Tipo de Extensión | Conteo | Ejemplo |
|---|---|---|
| WebGL estándar | ~30 | OES_texture_float |
| WEBGL_* | ~15 | WEBGL_debug_renderer_info |
| EXT_* | ~15 | EXT_texture_filter_anisotropic |
| Específicas del navegador | ~5 | MOZ_WEBGL_* |
Entropía: El conjunto exacto y el conteo de extensiones crea un hash.
Mitigación:
const originalGetExtension = WebGLRenderingContext.prototype.getExtension
WebGLRenderingContext.prototype.getExtension = function(name) {
if (name === 'WEBGL_debug_renderer_info') return null
if (name.startsWith('MOZ_')) return null
return originalGetExtension.call(this, name)
}
Conclusiones para Obscura
| Vector | Control del Proxy | Estrategia de Mitigación |
|---|---|---|
| Enumeración de fuentes | Parcial | Redondear mediciones, bloquear Flash |
| Soporte de características | Ninguno | Aceptar — inherente al navegador |
| Media queries CSS | Parcial | Bloquear exfiltración (DNS/red) |
| ClientRects | Parcial | Redondear valores mediante inyección JS |
| WebGPU | Parcial | Bloquear información del adapter |
| Service Workers | Parcial | Anular APIs de registro |
| Extensiones WebGL | Parcial | Filtrar lista de extensiones |