Descargar la lista de verificación en formato Excel
El contenido de este archivo es el mismo que la lista de verificación previa al lanzamiento del capítulo 4 (8 categorías, 32 elementos). La única diferencia es que incluye campos para rellenar Status y Notes, y que tiene dos hojas, Checklist-ja y Checklist-en, con el japonés y el inglés. Basta con usar el capítulo 4 para revisar mientras se lee, y la versión Excel para distribuirla como registro de revisión.
Cuando se habla de seguridad en aplicaciones de Windows, el alcance tiende a crecer rápidamente. Confianza cero, EDR, SBOM (Software Bill of Materials, lista de materiales de software), gestión de certificados, gestión de vulnerabilidades. Todo esto es importante, pero en la práctica hay muchos aspectos básicos que conviene no descuidar antes de llegar ahí.
En particular, en aplicaciones como las siguientes, cerrar las brechas básicas rinde más que aplicar “defensas avanzadas” desde el principio.
- Aplicaciones de escritorio WPF / WinForms / WinUI
- Aplicaciones Win32 en C++ / C#
- Integración con equipos, integración de archivos, conexión a bases de datos, herramientas de distribución interna
- Aplicaciones empresariales con mecanismo de actualización automática
- Configuraciones que incluyen servicios de Windows o EXE auxiliares
En el desarrollo de aplicaciones de Windows, es más realista empezar por no dejar agujeros claramente peligrosos que intentar perfeccionarlo todo de una vez. Aquí se organizan, en el orden de diseño, implementación, distribución y operación, los puntos mínimos que conviene no pasar por alto, de forma fácil de verificar.
1. Primero, la conclusión
- Lo primero que no conviene descuidar es no solicitar privilegios de administrador innecesarios, firmar el código, no mantener información confidencial en texto plano y no deshabilitar la verificación de certificados.
- En las aplicaciones de Windows, el propio material distribuido es la superficie de ataque. Es más seguro considerar también EXE / DLL / MSI / MSIX / el módulo de actualización automática.
ServerCertificateValidationCallback => true, las cadenas de conexión en texto plano, la carga descuidada medianteLoadLibrary("foo.dll")y la ejecución de SQL por concatenación de cadenas son puntos que conviene evitar incluso en el nivel mínimo.- Si solo una parte del procesamiento requiere privilegios de administrador, es más seguro no elevar toda la aplicación, sino separar únicamente esa parte en un EXE o servicio distinto.
- Para las aplicaciones distribuidas en Windows conviene partir de la base de firma + marca de tiempo. Esto no solo aporta confianza a los usuarios, sino que también facilita la detección de manipulaciones y las explicaciones operativas.
- La información confidencial almacenada debe protegerse alternando, según el uso, entre DPAPI / ProtectedData y Credential Locker. Como mínimo, conviene abandonar la práctica de dejarla en texto plano en
appsettings.json. - Los registros no son mejores cuanto más abundantes. Dejar tokens, contraseñas, cadenas de conexión, información personal o el cuerpo completo de las solicitudes tal cual convierte al propio registro en el protagonista del incidente.
La seguridad mínima consiste menos en añadir funciones especiales que en no dejar comportamientos predeterminados peligrosos ni implementaciones descuidadas.
2. Alcance de este artículo y qué significa “mínimo”
2.1. Alcance considerado
Este artículo está pensado para aplicaciones de Windows como las siguientes.
- Aplicaciones de escritorio WPF / WinForms / WinUI
- Aplicaciones Win32 en C++ / C#
- Herramientas de distribución interna, herramientas de integración con equipos, herramientas de monitorización
- Configuraciones que incluyen EXE auxiliares, servicios de Windows y actualizadores
- Software empresarial distribuido como EXE / MSI / MSIX
Aquí, “mínimo” no significa la forma final que supera una auditoría, sino los elementos cuya ausencia provoca incidentes con normalidad.
También conviene alinear las premisas de los ejemplos de código. Los ejemplos en C# suponen .NET 8 o posterior, y los de C++ suponen la API Win32. En .NET Framework 4.8 la idea general se mantiene, pero hay puntos donde la forma de escribir recomendada ha cambiado. Un ejemplo representativo es lo relacionado con ServicePointManager en 3.6, donde el código nuevo parte de usar IHttpClientFactory y HttpClient. Al revisar código de generaciones anteriores, tenga esto en cuenta.
2.2. Fuera de alcance
Por otro lado, hay temas que quedan fuera del núcleo de este artículo.
- El diseño de confianza cero de toda la empresa
- La operación integral de EDR / SIEM / DLP / MDM
- El endurecimiento detallado de controladores en modo kernel
- El diseño criptográfico desde cero
- Los procedimientos avanzados de análisis de amenazas o forense
Es decir, no se trata de “las grandes iniciativas de seguridad de toda la organización”, sino de la línea base que los desarrolladores de aplicaciones de Windows difícilmente pueden dejar de cubrir por sí mismos antes del lanzamiento.
3. La lista de verificación que conviene mirar primero
Antes de entrar en los detalles, primero se presenta una tabla que permite ver el conjunto. Solo con esto ya se puede tener una idea de dónde revisar.
3.1. Panorama general
| Elemento a verificar | Lo mínimo que hay que hacer | NG típico |
|---|---|---|
| Privilegios de ejecución | Usar asInvoker como predeterminado y separar solo el procesamiento que requiere elevación |
Configurar toda la aplicación como requireAdministrator |
| Confiabilidad del material distribuido | Firmar EXE / DLL / MSI / MSIX con firma de código, y añadir también la marca de tiempo | Distribuir sin firmar |
| Actualizaciones | Fijar el origen de las actualizaciones y detectar manipulaciones mediante HTTPS y verificación de firma | Sobrescribir directamente tras una descarga por HTTP |
| Información confidencial | No colocar secretos en el código fuente ni en configuración en texto plano; usar DPAPI / Credential Locker, etc. | Colocar claves de API o cadenas de conexión en texto plano en archivos de configuración |
| Comunicaciones | Usar HTTPS y no deshabilitar la verificación de certificados | Omitir siempre la verificación de certificados con return true |
| Entradas externas | Validar por completo SQL, archivos, IPC, URI, CSV, JSON, etc. | Dejarlas pasar sin más porque “es una herramienta interna” |
| Carga de DLL | Usar rutas absolutas, SetDefaultDllDirectories y un orden de búsqueda seguro |
Dejar LoadLibrary("foo.dll") a merced del directorio actual |
| Registros | Enmascarar tokens, contraseñas y PII, y diferenciar los mensajes de error para el usuario | Mostrar y guardar tal cual los detalles de excepciones o las cadenas de conexión |
| Dependencias | Actualizar de forma continua el SDK, NuGet, el runtime de VC++ y las dependencias OSS | Fijarlas durante años sin seguir la información de vulnerabilidades |
3.2. Los privilegios se basan en asInvoker
Esto es lo primero que conviene revisar en una aplicación de Windows. Si toda la aplicación se ejecuta con privilegios de administrador, los errores, la sustitución de DLL, la lectura incorrecta de archivos de configuración y las deficiencias en las entradas externas se ejecutan directamente con privilegios elevados.
La política básica es la siguiente.
- Las aplicaciones de UI habituales usan
asInvoker - Solo el procesamiento que requiere privilegios de administrador se separa en un proceso o servicio distinto
- La elevación se realiza únicamente en el momento necesario
- También se valida la entrada que se pasa al EXE auxiliar o al servicio
Si se trata de una aplicación de escritorio que normalmente solo se usa para consultar y editar, y solo la instalación o el cambio de configuración del firewall requieren privilegios de administrador, es más seguro desplazar solo la parte que requiere elevación a un broker, en lugar de configurar toda la aplicación como requireAdministrator.
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
“Es más cómodo si funciona como administrador” suele pasar factura más adelante. Ejecutar con el mínimo privilegio posible y aislar solo las operaciones que realmente lo requieren reduce considerablemente el radio del incidente.
3.3. Firmar los binarios y el instalador
En Windows, la confiabilidad del material distribuido es determinante. Lo que el usuario toca no es el código fuente, sino el EXE, el DLL, el MSI, el MSIX y el actualizador. Si esto no está firmado, tanto la explicación operativa como la detección de manipulaciones y la sensación de seguridad al distribuir se debilitan.
Como mínimo, conviene revisar lo siguiente.
- Firmar EXE / DLL / MSI / MSIX
- Firmar no solo el instalador, sino también los binarios auxiliares usados para actualizar
- Añadir marca de tiempo
- Incluir la fecha de caducidad del certificado y el procedimiento de renovación en el proceso de lanzamiento
En particular, una firma sin marca de tiempo suele causar problemas al verificarla después de que caduque el certificado. En lugar de pensar “ya está firmado, con eso basta”, es más estable incluir firma + marca de tiempo en el procedimiento de lanzamiento.
Si se usa MSIX, la firma del paquete es un requisito previo. En la distribución con MSI / EXE, también conviene firmar al menos el propio instalador y los binarios ejecutables principales.
3.4. Fijar la ruta de actualización e introducir detección de manipulaciones
En las aplicaciones de Windows actuales, la ruta de actualización se usa durante más tiempo que la instalación inicial. Si esto se descuida, aunque el cuerpo principal se haya construido con cuidado, el actualizador se convierte en el eslabón más débil.
En torno a las actualizaciones, conviene tener en cuenta al menos estos 5 puntos.
- La obtención de los archivos de actualización parte de HTTPS
- Verificar la firma o el hash del material actualizado descargado
- Evitar que la URL de origen de las actualizaciones pueda sustituirse sin límite desde el código o la configuración
- Firmar también el propio módulo de actualización
- Definir el procedimiento de reversión (rollback) o de recuperación en caso de fallo
Si se puede optar por MSIX + App Installer, es más fácil delegar el mecanismo de actualización en el sistema operativo. Por otro lado, si se cuenta con un actualizador propio, es necesario confirmar tanto la seguridad de la comunicación como la autenticidad del material distribuido. Con HTTPS solo se protege la “ruta de comunicación”, pero no se garantiza que “ese archivo sea realmente su propia publicación”.
3.5. No colocar información confidencial en el código fuente ni en configuración en texto plano
Este es un punto donde en la práctica realmente ocurren incidentes. Por pensar “es una herramienta interna” o “de todos modos solo se distribuye el exe”, se suelen colocar cadenas de conexión, claves de API, credenciales de carpetas compartidas o tokens fijos en el código fuente o en archivos de configuración.
Como mínimo, conviene evitar estas formas de almacenamiento.
- Claves de API escritas directamente en el código fuente
- Contraseñas en texto plano en
appsettings.jsonoapp.config - Cadenas de conexión incluidas en el repositorio
- Un diseño que coloca la clave de descifrado y el texto cifrado en el mismo lugar
- Credenciales fijas comunes para todos los usuarios en lugar de credenciales por usuario
En una aplicación de Windows, las opciones realistas se reducen básicamente a estas 4.
- Desea guardar credenciales de Windows Si es una packaged desktop app o de la familia WinUI, considere Credential Locker
- Desea cifrar y guardar secretos de forma local
En Win32 / .NET, use DPAPI /
ProtectedData - El destino de conexión puede usar autenticación de Windows o autenticación integrada Si es posible, evite que la aplicación tenga que manejar contraseñas
- Puede gestionar los secretos en la nube o en el servidor Priorice un diseño que no incruste secretos de larga duración en el cliente
En C#, aunque solo sea usando DPAPI de la siguiente manera, la mejora respecto al almacenamiento en texto plano ya es considerable. Si se escribe el ciclo completo de guardado y lectura, queda así.
// C# / .NET 8. ProtectedData es exclusivo de Windows, y en .NET
// requiere el paquete NuGet System.Security.Cryptography.ProtectedData.
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
public static class SecretStore
{
// Se necesita el mismo valor al descifrar. Funciona incluso con null, pero es más seguro definirlo.
private static readonly byte[] Entropy = [0x4b, 0x53, 0x2d, 0x76, 0x31];
public static void Save(string path, string secretText)
{
byte[] plaintext = Encoding.UTF8.GetBytes(secretText);
byte[] ciphertext = ProtectedData.Protect(
plaintext,
Entropy,
DataProtectionScope.CurrentUser);
// Se puede escribir la secuencia de bytes tal cual en el archivo, pero si se coloca en un archivo de configuración conviene usar Base64.
File.WriteAllText(path, Convert.ToBase64String(ciphertext));
CryptographicOperations.ZeroMemory(plaintext);
}
public static string Load(string path)
{
byte[] ciphertext = Convert.FromBase64String(File.ReadAllText(path));
// Si no coincide con el mismo usuario y el mismo entropy usados al guardar, se produce CryptographicException.
byte[] plaintext = ProtectedData.Unprotect(
ciphertext,
Entropy,
DataProtectionScope.CurrentUser);
try
{
return Encoding.UTF8.GetString(plaintext);
}
finally
{
CryptographicOperations.ZeroMemory(plaintext);
}
}
}
El lado que la invoca queda así.
string path = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"SampleApp",
"token.dat");
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
SecretStore.Save(path, "example-api-key");
string restored = SecretStore.Load(path);
Lo importante aquí no es “está cifrado, así que es seguro”, sino decidir mediante el diseño quién puede descifrarlo.
El significado cambia bastante según se use CurrentUser o LocalMachine. Con CurrentUser solo puede descifrar el usuario que guardó el dato, mientras que con LocalMachine puede descifrarlo cualquiera de la misma máquina. Si se ejecuta como servicio con una cuenta distinta, o si hay una operación de cambio de usuario, es necesario decidir esto de antemano; de lo contrario, más adelante se topará con “no se puede leer” o, al contrario, con “se puede leer de más”.
Además, dado que DPAPI guarda la clave en el perfil del usuario, la documentación indica explícitamente que el descifrado puede fallar cuando el perfil no está cargado (por ejemplo, durante una suplantación/impersonation). Si se plantea usarlo desde un servicio, conviene confirmar también este punto.
Para conexiones a SQL Server, en entornos on-premises a veces se puede tomar la autenticación de Windows como primera opción.
Si de todos modos es necesario incluir credenciales en la cadena de conexión, es más seguro al menos mantener Persist Security Info=False y no dejarlas en un archivo de configuración en texto plano.
3.6. La comunicación parte de HTTPS, sin matar la verificación de certificados
Un atajo introducido “solo para el desarrollo” que termina quedándose tal cual en producción. Los incidentes relacionados con las comunicaciones suelen seguir este patrón.
En particular, este tipo de código o configuración tiende a quedar en el producto final.
ServicePointManager.ServerCertificateValidationCallback += ... => trueHttpClientHandler.DangerousAcceptAnyServerCertificateValidator- Enviar el producto con la verificación de revocación de certificados deshabilitada
- Dejar en producción código que asume un certificado autofirmado de desarrollo
La política mínima es simple.
- Las comunicaciones en producción usan HTTPS
- No omitir siempre la verificación de certificados
- Si excepcionalmente se necesita relajar la verificación, limitarla al host y al certificado en cuestión
- Excluir con seguridad el código de bypass de desarrollo mediante condiciones de compilación o configuración
- En .NET, tener presente también la verificación de revocación
Un ejemplo de lo que no debe hacerse es, básicamente, así.
ServicePointManager.ServerCertificateValidationCallback +=
(_, _, _, _) => true;
A simple vista parece cómodo, pero esto se comporta casi como “esta comunicación HTTPS se acepta con cualquiera con quien se conecte”. Si se elimina la verificación de certificados, aunque se use HTTPS, el contenido queda bastante desprotegido.
La forma de escribirlo cambia según qué versión de .NET se asuma
Colocar la configuración global en ServicePointManager es una forma de escribir propia de la época de .NET Framework. En código nuevo, es más natural recibir el HttpClient desde IHttpClientFactory y, si se necesita configuración relacionada con TLS, colocarla en SocketsHttpHandler o en HttpClientHandler.
Sin embargo, dejarlo así pensando “es una API antigua, ya no debe tener efecto” es peligroso. La documentación de Microsoft indica que ServicePointManager.ServerCertificateValidationCallback se asigna, a partir de .NET 9, al RemoteCertificateValidationCallback de SocketsHttpHandler.SslOptions. Es decir, una sola línea con => true en algún lugar puede terminar afectando también a las comunicaciones de HttpClient.
Cuando excepcionalmente se desee relajar la verificación, en lugar de una configuración global que afecta a todo el proceso, conviene limitarla exclusivamente a ese handler.
// C# / .NET 8. Ejemplo que trata como excepción solo un host y un certificado concretos.
// Aunque se relaje para desarrollo, si no se limita el objetivo, es como matarla globalmente.
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
// Huella digital del certificado en cuestión. También se puede leer desde la configuración.
const string ExpectedThumbprint = "2B0C4E6A8D1F3B5D7F9A1C3E5A7C9E1B3D5F7A91";
var handler = new HttpClientHandler
{
CheckCertificateRevocationList = true, // Comprueba la revocación. El valor predeterminado es false
ServerCertificateCustomValidationCallback = (request, certificate, chain, errors) =>
{
if (errors == SslPolicyErrors.None)
{
return true;
}
// Solo se admite un único caso: "este equipo no confía en la CA interna".
// No se admite que el certificado no llegue o que el nombre de host no coincida
if (errors != SslPolicyErrors.RemoteCertificateChainErrors)
{
return false;
}
// Se fija tanto el destino como el certificado
if (request.RequestUri?.Host != "device.internal.example"
|| certificate is null
|| !string.Equals(certificate.Thumbprint, ExpectedThumbprint,
StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Ni siquiera el único certificado anclado (pinned) se acepta si está caducado o revocado.
// Aceptarlo abriría la puerta a seguir usando "un certificado revocado por fuga de la clave"
return chain is not null
&& chain.ChainStatus.All(s =>
s.Status is X509ChainStatusFlags.NoError
or X509ChainStatusFlags.UntrustedRoot
or X509ChainStatusFlags.PartialChain);
},
};
using var client = new HttpClient(handler);
ExpectedThumbprint se proporciona como una constante o desde la configuración, con la huella digital del certificado en cuestión.
Lo importante aquí es no “hacer como que no se ven” errors y chain.ChainStatus. Si se devuelve true solo porque la huella digital coincide, se sigue aceptando ese certificado incluso si ha caducado, o incluso después de que se haya revocado por una fuga de la clave. El anclaje (pinning) significa “confiar solo en este certificado”, no “confiar en este certificado pase lo que pase”. En el código anterior, lo único que se admite es UntrustedRoot y PartialChain (es decir, que la CA interna no esté instalada en este equipo); NotTimeValid (caducado) o Revoked (revocado) se rechazan directamente.
Para comprobar la revocación se necesita CheckCertificateRevocationList = true (el valor predeterminado es false, y la revocación no se verifica). A la inversa, si la CA interna no publica ni CRL ni OCSP, se rechazará con RevocationStatusUnknown. Ese es el comportamiento correcto. Si no se dispone de un medio para verificar la revocación, cúbralo mediante un periodo de validez del certificado más corto o una vía preparada de antemano para redistribuir el valor anclado. Lo más peligroso es “anclar un certificado de larga duración sin poder revocarlo nunca”.
3.7. Tratar toda entrada externa como “entrada no confiable”
Como las aplicaciones de Windows no son aplicaciones web, la validación de entradas tiende a ser laxa. Pero en la práctica, los puntos de entrada externos son más numerosos de lo que se piensa.
- Rutas de archivo
- CSV / Excel / JSON / XML
- Argumentos de línea de comandos
- named pipe / socket / COM / RPC / gRPC
- Cadenas que se pasan a la base de datos
- Valores del registro
- Portapapeles
- URL / deep link
- Datos devueltos por equipos externos o SDK
En particular, hay 3 puntos que no conviene descuidar en el nivel mínimo.
- El SQL siempre debe parametrizarse No construir el SQL por concatenación de cadenas.
- Las rutas de archivo deben normalizarse antes de usarse No usar directamente una ruta indicada por el usuario para eliminar, sobrescribir o expandir.
- La lectura de archivos externos debe incluir un límite de tamaño y una comprobación de formato “Se pudo abrir” no equivale a “es seguro”.
En cuanto al ejemplo de SQL, esto es lo que se quiere evitar.
var sql = "SELECT * FROM Users WHERE Name = '" + userName + "'";
Como mínimo, conviene acercarse a esto.
using System.Data;
using Microsoft.Data.SqlClient;
using var cmd = connection.CreateCommand();
cmd.CommandText = "SELECT * FROM Users WHERE Name = @name";
cmd.Parameters.Add("@name", SqlDbType.NVarChar, 256).Value = userName;
Suponer que “las entradas son confiables porque es una herramienta interna” es una premisa bastante peligrosa. En la realidad, llegan con normalidad CSV corruptos, nombres de archivo inesperados, datos antiguos de la base de datos, errores de entrada manual del operador o JSON a medio construir escrito por otra herramienta.
3.8. No dejar ambiguo el origen de carga de los DLL
Esta es una trampa muy propia de Windows.
Si se carga un DLL solo por su nombre, como en LoadLibrary("foo.dll"), según el orden de búsqueda puede terminar cargándose un DLL de una ubicación no prevista.
Lo que hay que hacer está bien definido.
- Cuando sea posible, especificar la ruta absoluta del DLL
- Configurar
SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS)en una etapa temprana - Añadir explícitamente los objetivos de búsqueda con
AddDllDirectory - Evitar diseños que pasen el resultado de
SearchPathdirectamente aLoadLibrary - No depender por completo del safe DLL search mode
Por ejemplo, en código nativo, un diseño sólido consiste en incluir lo siguiente en una etapa temprana de la inicialización del proceso.
SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);
Y luego registrar con AddDllDirectory únicamente los directorios adicionales necesarios.
Como esto “normalmente funciona”, tiende a quedar descuidado, pero si el directorio de trabajo cambia en el sitio de distribución, o si hay un DLL de otro producto en el PATH, se rompe de forma silenciosa. No solo aporta seguridad; también resulta bastante eficaz para prevenir fallos.
3.9. No exponer secretos en los registros ni en las excepciones
Aumentar los registros para investigar incidencias es importante. Pero los registros también tienden a convertirse en el cementerio de los secretos.
En torno a los registros, conviene revisar al menos lo siguiente.
- No sacar contraseñas, Bearer token ni claves de API en los registros
- No mostrar la cadena de conexión completa
- Enmascarar la información personal y el cuerpo de los datos de negocio
- Separar los detalles de la excepción entre la pantalla para el usuario y el registro interno
- No habilitar en producción registros de PII pensados para depuración
- Revisar los permisos del destino de guardado de dumps y trazas
En las versiones recientes de .NET también resulta más fácil organizar todo esto partiendo de la redacción (redaction). Como mínimo, conviene dejar de “convertir cualquier cosa a texto y registrarla tal cual”.
Algunos fallos habituales son estos.
- Guardar el cuerpo completo de la solicitud/respuesta HTTP
- Sacar el token o las cabeceras completas cuando falla la autenticación
- Mostrar el mensaje de excepción tal cual en un MessageBox
- Incluir todos los registros confidenciales en el ZIP de mantenimiento
La presentación de errores se separa, por ejemplo, así.
- Para el usuario: “No se pudo conectar con el servidor. Compruebe la configuración de red y la URL.”
- Registro interno: host de destino del fallo, tipo de error TLS, ID de correlación, stack trace, número de reintentos
Solo con esta separación, el equilibrio entre la fuga de información y la capacidad de investigación mejora considerablemente.
3.10. No dejar de lado las bibliotecas dependientes ni las herramientas de desarrollo
El último punto es discreto, pero de gran impacto. Aunque el cuerpo de la aplicación se construya con esmero, si se mantienen runtimes antiguos o bibliotecas dependientes con vulnerabilidades conocidas, el terreno se debilita.
Los elementos a revisar en sí no son muchos.
- Mantener el SDK / runtime de .NET en una versión con soporte
- Confirmar periódicamente las actualizaciones de dependencias de NuGet / OSS
- En C++, gestionar las versiones de los redistribuibles del runtime y de los DLL externos
- Incluir la confirmación de información de vulnerabilidades en la verificación previa al lanzamiento
- Preparar pruebas de humo (smoke test) para no romperse con las actualizaciones de dependencias
Aquí, “lo hago todo junto más tarde” es lo más peligroso. Si se descuida durante medio año o un año, la diferencia de actualización se vuelve demasiado grande, y la propia respuesta de seguridad termina convirtiéndose en un trabajo pesado.
3.11. Cómo verificar cada elemento
Una lista de verificación solo funciona junto con su método de comprobación. Para los elementos de 3.2 a 3.10, se enumeran a continuación comprobaciones que se pueden ejecutar realmente antes del lanzamiento.
| Qué se quiere confirmar | Método de confirmación |
|---|---|
| Si se solicita elevación | Ver el valor de requestedExecutionLevel en el manifiesto de la aplicación. En el código fuente es app.manifest; en el material distribuido, confirmarlo con Sigcheck de Sysinternals o con un editor de recursos |
| Firma y marca de tiempo | Ejecutar Get-AuthenticodeSignature .\app.exe en PowerShell y comprobar si Status es Valid y si TimeStamperCertificate está presente. Revisar también, uno por uno, los DLL incluidos y el updater |
| Si se ha matado la verificación de certificados | Buscar en todo el código fuente ServerCertificateValidationCallback, DangerousAcceptAnyServerCertificateValidator, ServerCertificateCustomValidationCallback y CheckCertificateRevocationList |
| Información confidencial escrita directamente | Buscar Password=, ApiKey, Secret, Token, ConnectionString. Incluir no solo el código fuente actual, sino también el historial del repositorio |
| Construcción del SQL | Buscar concatenaciones de cadenas que contengan "SELECT, "INSERT, + , y confirmar si pasan por Parameters.Add |
| Origen de carga de los DLL | Con Process Monitor, filtrar por el proceso en cuestión y aplicar los filtros Path ends with .dll y Result is NAME NOT FOUND. Como se ve dónde y en qué orden se buscó, se puede confirmar si se está mirando alguna carpeta no prevista |
| Vulnerabilidades conocidas en las dependencias | Ejecutar dotnet list package --vulnerable --include-transitive |
| Si hay secretos expuestos en los registros | Tras ejecutar la aplicación una vez, buscar en los registros generados Bearer , Password y Authorization |
La búsqueda en el código puede hacerse tanto con rg (ripgrep) como con la búsqueda de Visual Studio. Para ejecutarlo todo de una vez, esta es la forma.
Get-ChildItem -Recurse -Include *.cs,*.vb,*.cpp,*.h,*.config,*.json |
Select-String -Pattern 'ServerCertificateValidationCallback|DangerousAcceptAnyServerCertificateValidator|Password=|ApiKey' |
Select-Object Path, LineNumber, Line
Lo importante es dejar registro de que se hizo la comprobación. Si se deja constancia de que “se buscó” y de que “hubo 0 resultados”, en el siguiente lanzamiento bastará con revisar solo la diferencia.
4. Lista de verificación previa al lanzamiento
Se presenta en un formato listo para usar como plantilla de revisión o de decisión de envío. Para que sea fácil de confirmar en tabla, los elementos mínimos que conviene revisar antes del lanzamiento se agrupan por categoría.
4.1. Privilegios, forma de ejecución
| Elemento a verificar | Confirmación | Notas |
|---|---|---|
El inicio normal se ejecuta con asInvoker |
□ | |
| El procesamiento que requiere privilegios de administrador está separado en un EXE / servicio distinto, etc. | □ | |
| Si se usa un servicio, no se ha configurado con una cuenta de ejecución más privilegiada de lo necesario | □ | |
Las responsabilidades bajo %ProgramFiles% y bajo los datos de usuario están separadas |
□ |
4.2. Distribución, firma
| Elemento a verificar | Confirmación | Notas |
|---|---|---|
| Se firma el EXE / DLL / MSI / MSIX / updater | □ | |
| La firma incluye marca de tiempo | □ | |
| La fecha de caducidad del certificado y el procedimiento de renovación están incluidos en el flujo de lanzamiento | □ | |
| Está definido el método de verificación de hash o de detección de manipulaciones del material distribuido | □ |
4.3. Actualizaciones
| Elemento a verificar | Confirmación | Notas |
|---|---|---|
| La obtención de actualizaciones se realiza por HTTPS | □ | |
| Se verifica la firma o el hash tras la descarga | □ | |
| El diseño impide que la URL de origen de las actualizaciones se sustituya con facilidad | □ | |
| Existe una política de reversión o de reintento en caso de fallo de actualización | □ |
4.4. Información confidencial
| Elemento a verificar | Confirmación | Notas |
|---|---|---|
| No se escriben directamente en el código fuente contraseñas, claves de API ni cadenas de conexión | □ | |
| No se colocan secretos en archivos de configuración en texto plano | □ | |
| Los secretos que deben guardarse localmente están protegidos con DPAPI / Credential Locker, etc. | □ | |
| Donde es posible, se recurre a la autenticación de Windows o a las credenciales del usuario | □ |
4.5. Comunicaciones
| Elemento a verificar | Confirmación | Notas |
|---|---|---|
| Las comunicaciones en producción usan HTTPS | □ | |
No se deja en el producto final DangerousAcceptAnyServerCertificateValidator ni => true |
□ | |
| Se tiene en cuenta la verificación de revocación y la validación del nombre de host | □ | |
| No hay mezclado en producción código o configuración que asuma certificados de desarrollo | □ |
4.6. Entradas, acceso a datos
| Elemento a verificar | Confirmación | Notas |
|---|---|---|
| El SQL está parametrizado | □ | |
| Las entradas de línea de comandos, archivos, IPC, URI, etc. tienen límite y comprobación de formato | □ | |
| Las operaciones de rutas se normalizan para evitar la salida de la raíz | □ | |
| No se muestra el mensaje de excepción tal cual en pantalla | □ |
4.7. DLL y entorno de ejecución
| Elemento a verificar | Confirmación | Notas |
|---|---|---|
| El origen de carga de los DLL está indicado de forma explícita | □ | |
El orden de búsqueda se controla con SetDefaultDllDirectories / AddDllDirectory, etc. |
□ | |
| No se deja la carga de DLL a merced del directorio actual o del PATH | □ | |
| Se conoce el conjunto de archivos necesarios para la carga dinámica en el sitio de distribución | □ |
4.8. Registros, operación
| Elemento a verificar | Confirmación | Notas |
|---|---|---|
| No se sacan tokens, contraseñas ni PII en los registros | □ | |
| Se separan el registro interno y los mensajes dirigidos al usuario | □ | |
| Se han revisado los permisos del destino de guardado de dump / trace / log | □ | |
| Se confirma el estado de actualización del SDK y de las bibliotecas dependientes | □ |
5. NG habituales
En la práctica, lo que suele verse son más o menos estas ideas preconcebidas.
5.1. “Como es una herramienta interna, no hay problema”
Incluso en una herramienta interna hay con normalidad archivos corruptos, errores de operación, equipos personales, carpetas compartidas, DLL antiguos y configuraciones de permisos descuidadas. Aunque no esté publicada en internet, la superficie de ataque no desaparece.
5.2. “Como es HTTPS, es seguro”
HTTPS es importante, pero si se deshabilita la verificación de certificados, su sentido se debilita considerablemente. Además, en la distribución de actualizaciones no basta con HTTPS: también es necesaria la verificación de autenticidad del material distribuido.
5.3. “Como está cifrado, es seguro”
Si no están organizados el lugar donde se guarda la clave de descifrado, quién tiene permiso de descifrado, y los límites de usuario y de máquina, el cifrado por sí solo no basta.
En particular, si se usa un valor protegido con LocalMachine pensando que es “un secreto por usuario”, más adelante se producirá confusión.
5.4. “Con más registros se puede investigar”
Si los registros son abundantes pero dejan fluir tokens o información personal sin control, eso en sí mismo se convierte en un incidente. Si se quiere capacidad de investigación, lo primero es decidir qué se conserva y qué se oculta.
5.5. “Ejecutar como administrador lo soluciona”
Al principio resulta cómodo, pero después suele complicarse con UAC, la distribución, el soporte, los límites de privilegios, la carga de DLL y el destino de guardado de archivos. El mínimo privilegio es más estable a largo plazo.
6. Orden de prioridad general
Si hacerlo todo de golpe resulta demasiado pesado, el orden de prioridad es, en líneas generales, el siguiente.
El criterio de ordenación es la combinación entre la magnitud del daño si ocurre un incidente y el bajo coste de corregirlo. El capítulo 3 se ordena siguiendo el flujo de diseño “privilegios → distribución → implementación → operación”, pero aquí el orden es “de lo más peligroso a lo menos”, por lo que no coincide con el orden de los capítulos. Se indica el apartado correspondiente en cada punto.
- Revisión de los privilegios de administrador (3.2)
Primero, dejar de usar
requireAdministratorde forma habitual. El alcance del daño cambia de nivel, mientras que como cambio de diseño suele ser algo pequeño. - Firma y marca de tiempo (3.3) Ordenar la confiabilidad del material distribuido. Basta con incorporarlo al procedimiento, y si se añade más tarde requiere volver a redistribuir.
- Retirada de la información confidencial (3.5) Sacar los secretos del código fuente y de la configuración en texto plano. El daño si se filtran es grande, y una vez filtrados no hay vuelta atrás.
- Corrección de HTTPS + verificación de certificados (3.6)
Eliminar del producto final los patrones tipo
=> true. A menudo basta con eliminarlos para corregirlo, y si se descuida, toda la comunicación deja de ser confiable. - Revisión de las entradas SQL / archivos / IPC (3.7) Reducir la concatenación de cadenas y las entradas sin validar. Son numerosas, pero se pueden corregir de una en una.
- Fijación de la carga de DLL (3.8) Dejar de cargar solo por nombre y de depender del PATH. Es un trabajo de corregir una parte del procesamiento de inicio, y también sirve para prevenir fallos.
- Enmascaramiento de los registros (3.9) Evitar que los registros se conviertan en un daño secundario en caso de incidente. Como hay muchos puntos de salida, lleva tiempo.
- Regularización de la actualización de dependencias (3.10) Convertirlo en un flujo que se confirma en cada lanzamiento. No termina de una sola vez, así que el verdadero trabajo es convertirlo en un sistema.
La razón por la que la ruta de actualización (3.4) no aparece en este orden es que hay aplicaciones que no tienen actualización automática. Si la tiene, trátela con la misma prioridad que el punto 2, la firma. El módulo de actualización está en una posición más débil que el cuerpo principal, pero con capacidad de reescribirlo.
Con este orden resulta más fácil avanzar en el sentido de “primero cerrar los agujeros claramente peligrosos”.
7. Resumen
La seguridad en el desarrollo de aplicaciones de Windows cambia bastante solo con ordenar estos 7 puntos — privilegios, firma, información confidencial, comunicaciones, entradas, DLL y registros — antes de introducir productos especiales o mecanismos enormes.
Resumiendo la línea mínima en una frase cada punto, queda así.
- No ejecutar toda la aplicación con privilegios de administrador
- Firmar el material distribuido y las actualizaciones, y añadir marca de tiempo
- No colocar información confidencial en el código fuente ni en configuración en texto plano
- No matar la verificación de certificados aunque se use HTTPS
- No confiar en las entradas externas como SQL, archivos, IPC, etc.
- No dejar ambiguo el origen de carga de los DLL
- No exponer secretos en los registros
- No dejar de lado las bibliotecas dependientes
El tema de la seguridad es amplio, pero no hace falta abordarlo todo desde el principio. Sin embargo, el mínimo de no distribuir con comportamientos predeterminados peligrosos merece cubrirse en una etapa bastante temprana.
8. Referencias
- Administrator Broker Model - Win32 apps
- How User Account Control works
- Authenticode Digital Signatures
- Time Stamping Authenticode Signatures
- Sign a Windows app package
- Credential Locker for Windows apps
- CryptProtectData function (dpapi.h)
- CA5359: Do not disable certificate validation
- CA5399: Enable HttpClient certificate revocation list check
- Configuring parameters - ADO.NET Provider for SQL Server
- Connection String Syntax - ADO.NET
- Dynamic-Link Library Security - Win32 apps
- SetDefaultDllDirectories function (libloaderapi.h)
- Data redaction in .NET
- ProtectedData Class - Es exclusiva de Windows, y hay que tener en cuenta el comportamiento cuando el perfil no está cargado.
- ServicePointManager.ServerCertificateValidationCallback - A partir de .NET 9 se asigna a la configuración de SocketsHttpHandler.
- Comando dotnet list package - Con
--vulnerablese pueden confirmar las vulnerabilidades conocidas. - Get-AuthenticodeSignature
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Windows: cómo separar en el código «solo los procesos que necesitan permisos de administrador»
Diseño para mantener la UI de una app de Windows en asInvoker y aislar en un helper EXE los procesos que exigen permisos de administrador...
Almacenamiento de información confidencial en aplicaciones Windows - Cómo evitar la configuración en texto plano con DPAPI
Para no guardar en texto plano las cadenas de conexión ni los tokens de API en aplicaciones Windows, se repasan DPAPI / ProtectedData, la...
Las profundidades del I/O en Windows (6.ª entrega, final) ── Controladores de filtro y minifiltros: por qué Procmon y el análisis antivirus pueden interceptar el I/O
Última entrega de la serie sobre controladores de filtro y minifiltros de Windows: el Filter Manager, las altitudes, los callbacks pre/po...
Por qué Windows llegó a ser como es hoy: la evolución de las versiones de Windows vista por un desarrollador
Repasamos los cambios de Windows 95 a Windows 11 no como un cronograma visual, sino desde la perspectiva de un desarrollador: compatibili...
¿Dónde deben ir el catch y el log en el manejo de excepciones?
Organiza los límites para capturar excepciones, el log principal y quién decide la recuperación, evitando el catch amplio, los logs dupli...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Desarrollo de aplicaciones para Windows
Se trata de revisar toda la aplicación de Windows, incluido el diseño de permisos, el método de distribución, el mecanismo de actualización y el diseño de registros, por lo que encaja bien con el tema de Desarrollo de aplicaciones Windows.
Consultoría técnica y revisión de diseño
Si desea partir de una revisión de seguridad de una aplicación existente, de la organización de los límites de privilegios o del rediseño de la política del actualizador, esto puede plantearse como una consultoría técnica y revisión de diseño.
Preguntas frecuentes
Preguntas habituales en las consultas sobre el tema del artículo.
- ¿Qué es lo primero que hay que revisar en la seguridad de una aplicación de Windows?
- Son estos cuatro puntos: no solicitar privilegios de administrador innecesarios, firmar el código, no mantener información confidencial en texto plano y no deshabilitar la verificación de certificados. La seguridad mínima consiste menos en añadir funciones especiales que en no dejar comportamientos predeterminados peligrosos ni implementaciones descuidadas. Omitir siempre la verificación de certificados, usar cadenas de conexión en texto plano, cargar DLL confiando en el directorio actual y ejecutar SQL mediante concatenación de cadenas son puntos que conviene evitar incluso en el nivel mínimo.
- ¿No se debe ejecutar toda la aplicación con privilegios de administrador?
- Debe evitarse. Si toda la aplicación se ejecuta con privilegios de administrador, los errores, la sustitución de DLL, la lectura incorrecta de archivos de configuración y las deficiencias en las entradas externas se ejecutan directamente con privilegios elevados. La política básica consiste en usar asInvoker como predeterminado para las aplicaciones de UI habituales, separar en un proceso o servicio distinto solo el procesamiento que requiere privilegios de administrador, y elevar los privilegios únicamente en el momento necesario.
- ¿Dónde se deben almacenar las claves de API o las cadenas de conexión?
- Lo primero es dejar de guardarlas en texto plano en el código fuente o en archivos de configuración como appsettings.json. Para la información confidencial almacenada, conviene alternar entre DPAPI / ProtectedData y Credential Locker según el uso. Además, si se dejan tokens, contraseñas, cadenas de conexión o información personal tal cual en los registros, el propio registro se convierte en el protagonista del incidente, por lo que es necesario enmascararlos.
- ¿Es necesaria la firma de código incluso en aplicaciones de distribución interna?
- Conviene considerarla un requisito de partida. En las aplicaciones de Windows, el propio material distribuido (EXE / DLL / MSI / MSIX / módulo de actualización automática) es la superficie de ataque. Contar con firma de código + marca de tiempo aporta detección de manipulaciones, confianza para los usuarios y facilidad para explicar la operación. El mecanismo de actualización también debe fijar el origen de las actualizaciones y detectar manipulaciones mediante HTTPS y verificación de firma.
Perfil del autor
Página de presentación del autor del artículo.
Go Komura
Representante de KomuraSoft LLC
Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.