Cerramos el bloque de fundamentos con la fuga más silenciosa de todas. Guardaste bien los tokens, cifraste los archivos, sacaste los secretos del binario, y aun así el dato sensible se escapa por un canal que casi nadie audita: los logs. Un print del token para depurar, un cuerpo de respuesta completo en la consola, un valor marcado como público que se quedó ahí para producción. Nada de esto rompe la app, así que nadie lo nota, hasta que alguien lee esos logs.
Esta es la serie de fundamentos de seguridad; las tres partes anteriores cubrieron Keychain, Data Protection y las API keys.
Por qué los logs son un problema real
En iOS los logs no se quedan solo en tu Xcode. El sistema de logging unificado los persiste, y son legibles con la app Console conectando el dispositivo a un Mac, o dentro de un sysdiagnose que el usuario puede generar y compartir. Un crash report también puede arrastrar lo último que se registró. En otras palabras, lo que imprimes puede salir del dispositivo.
El primer paso es dejar de usar print para cualquier cosa que pueda tocar datos sensibles. print escribe a la salida estándar sin ningún control de privacidad, y en un build de release no aporta nada más que superficie de fuga. La herramienta correcta es os.Logger.
os.Logger y los niveles de privacidad
Logger es la API de logging unificado de Apple. Se crea una vez por subsistema y categoría:
import os
let logger = Logger(subsystem: "com.midominio.app", category: "auth")
Lo interesante es cómo trata los valores que interpolas. Cada valor tiene un nivel de privacidad, y el comportamiento por defecto no es uniforme:
- El texto estático del mensaje siempre se ve.
- Los números y escalares interpolados son públicos por defecto.
- Las cadenas dinámicas interpoladas se redactan por defecto: en un build de release aparecen como
<private>cuando las lee otro proceso.
Ese default para las cadenas es una buena red de seguridad, pero apoyarse en él es un error. La razón es lo que pasa cuando depuras:
// Durante el desarrollo, para ver el token en consola:
logger.debug("token recibido: \(token, privacy: .public)")
Marcaste el token como público para verlo mientras depurabas, y ese .public se quedó en el commit. En producción, ese token ahora es legible desde Console. Este es el patrón que causa la mayoría de las fugas por log: no el default, sino el .public que alguien puso para depurar y olvidó quitar.
La regla que recomiendo es marcar la privacidad de forma explícita siempre que un valor pueda ser sensible, en lugar de confiar en el default:
logger.info("login de usuario \(userID, privacy: .private)")
logger.error("fallo al refrescar el token: \(error.localizedDescription, privacy: .public)")
Para credenciales hay un nivel más estricto, .sensitive, pensado para datos que no deberían aparecer ni siquiera en escenarios donde .private se relajaría:
logger.log("respuesta de autenticación \(rawToken, privacy: .sensitive)")
Qué no registrar, nunca
Más allá de la mecánica, hay una lista corta de cosas que simplemente no deberían pasar por un log, marcadas como públicas o no:
- Tokens, contraseñas, claves, y cualquier credencial.
- Cuerpos completos de peticiones o respuestas. Es cómodo para depurar y es exactamente donde viajan los tokens y los datos personales.
- Datos personales del usuario: correo, teléfono, ubicación, identificadores.
Un cuidado extra con los SDK de analítica y de crash reporting: muchos capturan logs y breadcrumbs de forma automática. Si registras un dato sensible, puede terminar enviado a un tercero sin que lo hayas decidido conscientemente. Vale la pena revisar qué captura cada SDK que integras.
Dos fugas vecinas
Los logs no son el único canal por el que un dato sensible sale sin que lo pienses. Dos más que cierran bien este bloque.
El portapapeles
Cuando copias algo al portapapeles general, cualquier otra app puede leerlo. Para un dato sensible como un código de un solo uso o una contraseña, conviene marcarlo para que no se sincronice con otros dispositivos y expire pronto:
import UIKit
UIPasteboard.general.setItems(
[[UIPasteboard.typeAutomatic: codigo]],
options: [
.localOnly: true,
.expirationDate: Date().addingTimeInterval(60)
]
)
.localOnly evita que el valor viaje por el portapapeles universal a tu Mac u otro dispositivo, y .expirationDate hace que el sistema lo descarte pasado el tiempo que indiques.
La captura del multitarea
Cuando el usuario manda tu app a segundo plano, iOS toma una captura de la pantalla para mostrarla en el selector de apps. Si en ese momento hay un dato sensible visible, un saldo, un número de tarjeta, esa captura queda guardada. La solución es tapar la pantalla antes de que el sistema tome la foto, al pasar a inactivo:
func sceneWillResignActive(_ scene: UIScene) {
// Muestra una vista opaca que cubra el contenido sensible.
overlayWindow?.isHidden = false
}
func sceneDidBecomeActive(_ scene: UIScene) {
overlayWindow?.isHidden = true
}
Lo que llevamos
- Los logs salen del dispositivo vía Console, sysdiagnose y crash reports; deja de usar
printpara datos sensibles. - Con
os.Logger, marca la privacidad de forma explícita; el error clásico es un.publicde depuración olvidado en release. - Nunca registres tokens, credenciales, cuerpos completos ni datos personales, y revisa qué capturan tus SDK de terceros.
- Cierra las fugas vecinas: portapapeles local y con expiración, y tapa la pantalla antes de la captura del multitarea.
Qué sigue
Con esto quedan cubiertos los datos del usuario en el dispositivo: dónde guardar secretos, cómo cifrar archivos, qué hacer con las claves de API y cómo no filtrar por los logs. Pero hoy hay un canal más por el que la información sale de tu máquina, y lo abres tú cada vez que pegas código en un asistente de IA.
En la parte 5, que cierra el bloque de fundamentos, vemos cómo programar de forma asistida con Claude, ChatGPT y compañía sin filtrar datos: qué compartir y qué no, por qué un secreto nunca va al prompt, y cómo revisar el código que la IA te devuelve.
Fuentes: Logger (Apple) · OSLogPrivacy (Apple) · UIPasteboard (Apple)