Una lista di controllo minima sulla sicurezza per lo sviluppo di app Windows

· Aggiornato il: · · Sviluppo Windows, Sicurezza, Progettazione, C# / .NET, Win32

Scarica la versione Excel della checklist

Il contenuto di questo file è uguale alla checklist pre-rilascio del capitolo 4 (8 categorie, 32 voci). L’unica differenza è che include le colonne Status e Notes per la compilazione, ma i fogli sono solo in giapponese (Checklist-ja) e inglese (Checklist-en); non è inclusa una versione italiana. Se desideri verificarlo mentre leggi, usa il capitolo 4; se vuoi distribuirlo come record di revisione, usa la versione Excel.

I discorsi sulla sicurezza delle applicazioni Windows tendono a gonfiarsi rapidamente. Zero trust, EDR, SBOM, operazioni di certificati, gestione delle vulnerabilità. Tutto importante, ma in pratica ci sono diverse nozioni di base da non perdere prima.

Soprattutto per app come le seguenti, colmare le lacune nelle nozioni di base ripaga più delle “difese avanzate”:

  • Applicazioni desktop WPF / WinForms / WinUI
  • C++ / C# Applicazioni Win32
  • Integrazione dispositivi, integrazione file, connessioni DB, strumenti distribuiti internamente
  • App aziendali con meccanismo di aggiornamento automatico
  • Configurazioni che includono servizi Windows o helper EXEs

Nello sviluppo di app Windows, è più realistico non lasciare prima alcun buco evidentemente pericoloso piuttosto che provare a perfezionare tutto in una volta. Qui organizziamo i punti minimi da non perdere, in forma di lista di controllo, nell’ordine di progettazione, implementazione, distribuzione e operazioni.

1. Prima la conclusione

  • Le prime cose da non perdere sono: non richiedere privilegi di amministratore inutilmente, firmare i file binari, non mantenere segreti in testo normale e non disabilitare la convalida del certificato.
  • Per un’app Windows, gli stessi artefatti della distribuzione sono una superficie di attacco. È più sicuro guardare tutto: EXE / DLL / MSI / MSIX / moduli di aggiornamento automatico.
  • ServerCertificateValidationCallback => true, stringhe di connessione in testo normale, carichi imprudenti come LoadLibrary("foo.dll") e SQL costruiti mediante concatenazione di stringhe sono elementi da evitare anche alla barra minima.
  • Se solo una parte dell’elaborazione richiede diritti di amministratore, è più sicuro dividere solo quella parte in un EXE o servizio separato anziché elevare l’intera app.
  • Le app distribuite su Windows dovrebbero assumere firma + timestamp. Oltre alla fiducia dell’utente, semplifica anche il rilevamento delle manomissioni e le spiegazioni operative.
  • Per i segreti inattivi, scegli tra DPAPI / ProtectedData e Credential Locker a seconda del caso d’uso. Come minimo, vuoi sfuggire allo stato dei segreti in chiaro in appsettings.json.
  • Una maggiore registrazione non è automaticamente migliore. Perseverare token, password, stringhe di connessione, informazioni personali o corpi di richiesta completi così come sono e il log stesso diventa il protagonista dell’incidente.

La sicurezza minima riguarda meno l’aggiunta di funzionalità speciali e più il non fornire valori predefiniti pericolosi e implementazioni sciatte.

2. Scopo di questo articolo e cosa significa “minimo”.

2.1. Cosa trattiamo

Le app Windows che questo articolo ha in mente sono queste.

  • Applicazioni desktop WPF / WinForms / WinUI
  • C++ / C# Applicazioni Win32
  • Strumenti distribuiti internamente, strumenti di integrazione dei dispositivi, strumenti di monitoraggio
  • Configurazioni che includono servizi di supporto EXEs, Windows e programmi di aggiornamento
  • Software aziendale distribuito come EXE / MSI / MSIX

“Minimo” qui non significa uno stato finale che supera un controllo: significa elementi che, se mancanti, causeranno semplicemente incidenti.

2.2. Ciò che non copriamo

Alcune cose si collocano fuori dal centro di questo articolo.

  • Progettazione Zero Trust a livello di organizzazione
  • Funzionamento end-to-end di EDR / SIEM / DLP / MDM
  • Rafforzamento dettagliato dei driver del kernel
  • Progettare schemi crittografici da zero
  • Analisi avanzata delle minacce o procedure forensi

In altre parole, non stiamo trattando “colossi programmi di sicurezza a livello di organizzazione”, ma la linea di base che uno sviluppatore di app Windows può e deve proteggere da solo prima del rilascio.

3. La lista di controllo da esaminare innanzitutto

Prima della discussione dettagliata, ecco una tabella che offre l’intero panorama. Questo da solo è sufficiente per individuare da dove iniziare la revisione.

3.1. Il quadro generale

Elemento da controllare Azione minima Tipico anti-modello
Privilegi di esecuzione Predefinito su asInvoker; isolare solo le operazioni che necessitano di elevazione Contrassegnare l’intera app requireAdministrator
Affidabilità degli artefatti Codice EXE / DLL / MSI / MSIX, con marca temporale Spedizione non firmata
Aggiornamenti Blocca la fonte dell’aggiornamento; rilevare manomissioni tramite HTTPS e controlli di firma Download su HTTP e sovrascrittura sul posto
Segreti Mantieni i segreti fuori dal codice sorgente e dalla configurazione in chiaro; utilizzare DPAPI / Credential Locker ecc. Chiavi API e stringhe di connessione nei file di configurazione in testo semplice
Comunicazione Usa HTTPS; non disabilitare mai la convalida del certificato Salto permanente della convalida del certificato con return true
Ingresso esterno Convalida tutto: SQL, file, IPC, URIs, CSV, JSON Salutandolo “perché è uno strumento interno”
DLL caricamento Utilizza percorsi assoluti, SetDefaultDllDirectories e un ordine di ricerca sicuro Lasciando LoadLibrary("foo.dll") nella directory corrente
Registrazione Token maschera, password, PII; separare gli errori riscontrati dall’utente dai log interni Visualizzazione o persistenza dei dettagli dell’eccezione e delle stringhe di connessione così come sono
Dipendenze Aggiorna continuamente i runtime SDKs, NuGet, VC++, dipendenze OSS Congelare le versioni per anni e ignorare i rapporti sulle vulnerabilità

3.2. Privilegi predefiniti su asInvoker

Questa è la prima cosa da rivedere in un’app Windows. Esegui l’intera app con diritti di amministratore e bug, sostituzione DLL, file di configurazione letti erroneamente e input esterno non convalidato vengono tutti eseguiti con questi privilegi avanzati.

La politica di base è questa.

  • Le normali app dell’UI vengono eseguite come asInvoker
  • Solo le operazioni che necessitano dei diritti di amministratore vengono suddivise in un processo o servizio separato
  • Elevare solo per i momenti che ne hanno bisogno
  • Convalida l’input passato all’helper EXEs e anche ai servizi

Se la tua app desktop normalmente visualizza e modifica solo e solo le modifiche all’installazione o alla configurazione del firewall richiedono diritti di amministratore, è più sicuro inserire solo le parti con privilegi elevati in un broker anziché creare l’intera app requireAdministrator.

<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
  <security>
    <requestedPrivileges>
      <requestedExecutionLevel level="asInvoker" uiAccess="false" />
    </requestedPrivileges>
  </security>
</trustInfo>

“È più semplice se viene eseguito solo come amministratore” ritorna quasi sempre pungente. Esegui con minimi privilegi e ritaglia solo le operazioni che veramente ne richiedono di più, e il raggio di esplosione di qualsiasi incidente si ridurrà notevolmente.

3.3. Firma i tuoi file binari e programmi di installazione

Il Windows, l’affidabilità degli artefatti della tua distribuzione ha un peso reale. Ciò che gli utenti toccano non è il tuo codice sorgente: è EXE, DLL, MSI, MSIX e il programma di aggiornamento. Se non li firmi, la tua storia operativa, il rilevamento delle manomissioni e la fiducia nei tempi di distribuzione si indeboliranno.

Come minimo, guarda questi.

  • Segnaletica EXE / DLL / MSI / MSIX
  • Firma non solo il programma di installazione ma anche i binari di supporto utilizzati per gli aggiornamenti
  • Aggiungi timestamp
  • Includere le procedure di scadenza e rinnovo del certificato nel processo di rilascio

In particolare, le firme senza timestamp causano problemi durante la convalida dopo la scadenza del certificato. Invece di “è firmato, quindi abbiamo finito”, inserisci firma + timestamp nella procedura di rilascio per la stabilità.

Se usi MSIX, la firma del pacchetto è un dato di fatto. Anche con la distribuzione MSI / EXE, almeno il programma di installazione stesso e i principali file binari eseguibili dovrebbero essere firmati.

3.4. Blocca il canale di aggiornamento e aggiungi il rilevamento delle manomissioni

Per un’app Windows moderna, il canale di aggiornamento viene utilizzato molto di più nel corso della sua vita rispetto all’installazione iniziale. Se sbagli, per quanto attentamente tu abbia costruito il corpo dell’app, il programma di aggiornamento diventa il punto più debole.

Le cinque cose minime a cui pensare riguardo agli aggiornamenti:

  • Recupera i file di aggiornamento su HTTPS, sempre
  • Verificare la firma o l’hash degli artefatti di aggiornamento scaricati
  • Assicurarsi che la fonte di aggiornamento URL non possa essere scambiata arbitrariamente tramite codice o configurazione
  • Firmare il modulo di aggiornamento stesso
  • Decidere le procedure di rollback e ripristino in caso di errore

Se puoi adottare MSIX + App Installer, puoi spingere gran parte del meccanismo di aggiornamento verso OS. Se invece esegui il tuo programma di aggiornamento, devi verificare sia la sicurezza del trasporto che l’autenticità dell’artefatto. HTTPS protegge il canale, ma non garantisce che “questo file sia veramente qualcosa che abbiamo pubblicato”.

3.5. Mantieni i segreti fuori dal codice sorgente e dalla configurazione in chiaro

È qui che accadono davvero gli incidenti del mondo reale. “È uno strumento interno”, “stiamo solo distribuendo un exe” e stringhe di connessione, chiavi API, credenziali di cartelle condivise e token fissi finiscono nel codice sorgente o nei file di configurazione.

Come minimo, evita questi accordi.

  • Chiavi API codificate nel codice sorgente
  • Password in testo normale in appsettings.json o app.config
  • Stringhe di connessione archiviate nel repository
  • Progetti che mantengono la chiave di decrittazione e il testo cifrato nello stesso posto
  • Credenziali fisse condivise da tutti anziché per utente

Le opzioni realistiche per un’app Windows si riducono più o meno a queste quattro.

  • Vuoi archiviare le credenziali Windows Per le app desktop in pacchetto / WinUI, considera Credential Locker
  • Vuoi che i segreti siano crittografati localmente Per Win32 / .NET, utilizzare DPAPI / ProtectedData
  • La destinazione supporta l’autenticazione Windows o l’autenticazione integrata Se possibile, non fare in modo che l’app contenga una password
  • I segreti possono essere gestiti sul cloud o sul lato server Preferisci design che non incorporino segreti di lunga durata nel cliente

In C#, anche solo usare DPAPI in questo modo è già molto meglio dell’archiviazione di testo normale. Se scriviamo l’intero ciclo di salvataggio e caricamento, diventa così.

// C# / .NET 8. ProtectedData è specifico di Windows;
// su .NET serve il pacchetto NuGet System.Security.Cryptography.ProtectedData.
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;

public static class SecretStore
{
    // Serve lo stesso valore anche in fase di decrittografia. null funziona, ma è più sicuro specificarlo.
    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);

        // I byte grezzi andrebbero bene, ma se va in un file di configurazione meglio Base64.
        File.WriteAllText(path, Convert.ToBase64String(ciphertext));

        CryptographicOperations.ZeroMemory(plaintext);
    }

    public static string Load(string path)
    {
        byte[] ciphertext = Convert.FromBase64String(File.ReadAllText(path));

        // Se l'utente o l'entropy non corrispondono a quelli del salvataggio, scatta CryptographicException.
        byte[] plaintext = ProtectedData.Unprotect(
            ciphertext,
            Entropy,
            DataProtectionScope.CurrentUser);

        try
        {
            return Encoding.UTF8.GetString(plaintext);
        }
        finally
        {
            CryptographicOperations.ZeroMemory(plaintext);
        }
    }
}

Il lato chiamante:

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);

La cosa importante qui non è “è crittografato, quindi è sicuro” ma decidere nella progettazione chi può decrittografarlo. CurrentUser rispetto a LocalMachine cambia considerevolmente il significato. CurrentUser consente la decrittografia solo all’utente che ha salvato il segreto; LocalMachine consente la decrittografia a chiunque sullo stesso computer. Se il servizio gira con un account diverso o si cambia utente, questa scelta va decisa prima, altrimenti più avanti si blocca con “non riesco a leggere” o “leggo troppo”.

Inoltre DPAPI memorizza la chiave nel profilo utente, quindi la documentazione Microsoft avverte che la decrittografia può fallire quando il profilo non è caricato (ad esempio durante l’impersonificazione). Se si usa da un servizio, verificare anche questo punto.

Per le connessioni SQL Server, negli ambienti locali l’autenticazione Windows può spesso essere la prima scelta. Se devi assolutamente includere le credenziali nella stringa di connessione, mantieni almeno Persist Security Info=False e non lasciarle nei file di configurazione in testo normale.

3.6. HTTPS per impostazione predefinita e non interrompe mai la convalida del certificato

Un bypass ha aggiunto le navi “solo per lo sviluppo” alla produzione intatta. Questo è lo schema alla base della maggior parte degli incidenti legati alla comunicazione.

Il codice e le impostazioni che più spesso persistono nelle build spedite:

  • ServicePointManager.ServerCertificateValidationCallback += ... => true
  • HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
  • Spedizione con controlli di revoca del certificato disabilitati
  • Lasciare in produzione il codice che presuppone lo sviluppo di certificati autofirmati

La politica minima è semplice.

  • Il traffico di produzione utilizza HTTPS
  • Non saltare mai incondizionatamente la convalida del certificato
  • Se hai davvero bisogno di un’eccezione di convalida, limitala a host e certificati specifici
  • Elimina in modo affidabile il codice di bypass dello sviluppo tramite condizioni di build o configurazione
  • In .NET, tieni presente anche il controllo della revoca

Il cattivo esempio di solito assomiglia a questo.

ServicePointManager.ServerCertificateValidationCallback +=
    (_, _, _, _) => true;

Sembra conveniente, ma si comporta in modo simile a “lascia passare questa connessione HTTPS, indipendentemente da chi si connette”. Elimina la convalida del certificato e anche con HTTPS la sostanza è in gran parte svuotata.

La scrittura cambia a seconda del .NET presupposto

Impostare ServicePointManager globalmente è la modalità tipica dell’era .NET Framework. Per il codice nuovo, è più naturale ricevere HttpClient da IHttpClientFactory e tenere le impostazioni TLS sul lato di SocketsHttpHandler o HttpClientHandler.

Non però che “è una vecchia API, quindi probabilmente non ha effetto” e lasciarla stare: la documentazione Microsoft afferma che ServicePointManager.ServerCertificateValidationCallback viene mappato su SocketsHttpHandler.SslOptions.RemoteCertificateValidationCallback a partire da .NET 9. Una singola riga => true da qualche parte può quindi far passare anche le comunicazioni di HttpClient.

Quando vuoi allentare eccezionalmente la verifica, non usare un’impostazione globale valida per l’intero processo: chiudila dentro quel singolo handler.

// C# / .NET 8. Esempio che limita l'eccezione a un host e a un certificato specifici.
// Anche se lo allenti per sviluppo, senza delimitare il target sei allo stesso livello
// di uccidere la validazione globalmente.
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

// Thumbprint del certificato previsto. Può essere letto da configurazione.
const string ExpectedThumbprint = "2B0C4E6A8D1F3B5D7F9A1C3E5A7C9E1B3D5F7A91";

var handler = new HttpClientHandler
{
    CheckCertificateRevocationList = true,   // controlla la revoca. Il default è false
    ServerCertificateCustomValidationCallback = (request, certificate, chain, errors) =>
    {
        if (errors == SslPolicyErrors.None)
        {
            return true;
        }

        // Si ammette solo "questo dispositivo non si fida della CA interna".
        // Non si accettano certificato mancante o mismatch dell'host.
        if (errors != SslPolicyErrors.RemoteCertificateChainErrors)
        {
            return false;
        }

        // Fissa sia l'host che il certificato.
        if (request.RequestUri?.Host != "device.internal.example"
            || certificate is null
            || !string.Equals(certificate.Thumbprint, ExpectedThumbprint,
                              StringComparison.OrdinalIgnoreCase))
        {
            return false;
        }

        // Anche un certificato pinzato non può passare se scaduto o revocato.
        // Se ci si fida di un solo certificato, "chiave rubata => lo revochiamo"
        // deve funzionare. Si ammettono solo UntrustedRoot e PartialChain
        // (CA interna non presente sul dispositivo).
        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 va dato come costante o letto da configurazione, ma non hardcoded in modo opaco.

Il punto importante è non “non guardare” errors e chain.ChainStatus. Se restituisci true solo perché il thumbprint coincide, quel certificato passerebbe anche scaduto o revocato. Il pin non significa “qualsiasi cosa valga questo thumbprint va bene”, ma “mi fido di questa specifica chiave fintanto che lo stato della catena non dice il contrario”. Nel codice sopra si ammettono solo UntrustedRoot e PartialChain (= la CA interna non è installata sul dispositivo); NotTimeValid (scaduto) e Revoked (revocato) vengono rifiutati così come sono.

Per vedere la revoca è necessario CheckCertificateRevocationList = true (il default è false, quindi la revoca non viene controllata). Viceversa, se la CA interna non pubblica CRL né OCSP, RevocationStatusUnknown farà fallire la verifica. Questo è il comportamento corretto. Se non puoi predisporre un meccanismo di revoca, colma il buco riducendo la durata dei certificati o creando prima un percorso per ridistribuire il pin. “Pinzare un certificato a lunga scadenza senza possibilità di revoca” è lo stato più pericoloso.

3.7. Tratta tutti gli input esterni come non attendibili

Le app Windows non sono app Web, quindi la convalida dell’input diventa facilmente permissiva. Ma in realtà gli ingressi per input esterni sono più numerosi di quanto si pensi.

  • Percorsi dei file
  • CSV / Excel / JSON / XML
  • Argomenti della riga di comando
  • Tubi / prese con nome / COM / RPC / gRPC
  • Stringhe passate al DB
  • Valori di registro
  • Gli appunti
  • URLs / collegamenti diretti
  • Dati restituiti da dispositivi esterni o SDKs

I tre da non perdere assolutamente:

  1. Parametrizzare sempre SQL Non creare mai SQL mediante concatenazione di stringhe.
  2. Normalizza i percorsi dei file prima di utilizzarli Non utilizzare mai direttamente un percorso fornito dall’utente per eliminare, sovrascrivere o estrarre.
  3. Applica limiti di dimensione e controlli di formato durante la lettura di file esterni “Si è aperto, quindi è sicuro” non è una cosa.

Per SQL, questo è ciò che vuoi evitare:

var sql = "SELECT * FROM Users WHERE Name = '" + userName + "'";

Come minimo, spostalo in questo:

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;

“È uno strumento interno, quindi l’input è affidabile” è una premessa davvero pericolosa. In realtà, CSVs corrotto, nomi di file inaspettati, dati DB obsoleti, errori di battitura degli operatori e JSON incompleti scritti da altri strumenti arrivano continuamente.

3.8. Non lasciare mai ambigue le posizioni di carico di DLL

Questa è decisamente una trappola Windows. Carica un DLL solo per nome, ad esempio LoadLibrary("foo.dll") e, a seconda dell’ordine di ricerca, potresti ritirare un DLL da una posizione non prevista.

Le azioni sono ben consolidate.

  • Specificare il percorso assoluto di DLL ove possibile
  • Imposta SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS) in anticipo
  • Aggiungi posizioni di ricerca esplicite con AddDllDirectory
  • Evita progetti che superano i risultati SearchPath direttamente in LoadLibrary
  • Non fare affidamento esclusivamente sulla modalità di ricerca sicura DLL

Per il codice nativo, ad esempio, inserirlo nelle prime fasi dell’inizializzazione del processo è uno schema forte.

SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);

Quindi registra solo le directory aggiuntive di cui hai bisogno con AddDllDirectory.

Poiché “normalmente funziona”, quest’area viene trascurata e quindi la directory di lavoro cambia presso la sede di un cliente o il DLL di un altro prodotto appare in PATH e le cose si interrompono silenziosamente. Oltre alla sicurezza, ciò ripaga notevolmente in termini di prevenzione dei guasti.

3.9. Mantieni i segreti lontani dai log e dalle eccezioni

È importante aggiungere registri per l’indagine sugli incidenti. Ma i tronchi sono anche ottimi cimiteri per i segreti.

Gli elementi minimi di revisione relativi alla registrazione:

  • Non registrare password, token Bearer o chiavi API
  • Non registrare intere stringhe di connessione
  • Mascherare le informazioni personali e i dati aziendali
  • Separare i dettagli delle eccezioni tra l’UI rivolta all’utente e i registri interni
  • Non abilitare la registrazione PII di livello debug in produzione
  • Esaminare le autorizzazioni sulle posizioni dump / traccia storage

Il recente .NET semplifica notevolmente la progettazione incentrata sulla redazione. Per lo meno, smetti di “stringificare tutto e registrarlo così com’è”.

Alcuni classici fallimenti:

  • Persistenza dell’intero corpo della richiesta / risposta HTTP
  • Dumping del token o delle intestazioni complete in caso di errore di autenticazione
  • Visualizzazione dei messaggi di eccezione non elaborati in un MessageBox
  • Raggruppamento di ogni registro sensibile nella manutenzione ZIP

Presentazione separata degli errori come questa, ad esempio:

  • Rivolto all’utente: “Impossibile connettersi al server. Controlla le impostazioni di rete e URL.”
  • Log interno: host di destinazione, tipo di errore TLS, correlazione ID, analisi dello stack, conteggio dei tentativi

Questa separazione da sola migliora sostanzialmente l’equilibrio tra prevenzione delle perdite e diagnosticabilità.

3.10. Non trascurare le dipendenze e gli strumenti

L’ultimo capo è poco glamour ma di grande impatto. Costruisci il corpo dell’app con attenzione, ma spediscilo sopra un vecchio runtime o dipendenze con vulnerabilità note e il pavimento cede.

L’elenco delle cose da guardare è in realtà breve.

  • Mantieni il runtime .NET SDK / sulle versioni supportate
  • Esaminare periodicamente gli aggiornamenti delle dipendenze NuGet / OSS
  • Per C++, gestione della versione dei ridistribuibili runtime e DLLs esterni
  • Aggiungere i controlli dei rapporti sulle vulnerabilità all’elenco di controllo pre-rilascio
  • Mantieni i test del fumo in modo che gli aggiornamenti delle dipendenze non ti interrompano silenziosamente

“Lo faremo più tardi” è la posizione più pericolosa qui. Lascialo riposare per sei mesi o un anno e il delta degli aggiornamenti diventerà così grande che il lavoro di sicurezza stesso diventerà un progetto pesante.

3.11. Come verificare ogni voce

Una checklist funziona solo se è accompagnata dal metodo di verifica. Di seguito riportiamo i comandi che è possibile eseguire effettivamente prima del rilascio per le voci dalla 3.2 alla 3.10.

Cosa verificare Metodo
Non richiede elevazione Controllare il valore di requestedExecutionLevel nel manifest dell’app. In origine app.manifest; nella build distribuita usare Sigcheck di Sysinternals o un editor di risorse
Firma e timestamp In PowerShell eseguire Get-AuthenticodeSignature .\app.exe e verificare che Status sia Valid e TimeStamperCertificate sia presente. Controllare anche ogni DLL e l’updater
Non si è disabilitata la validazione del certificato Cercare in tutto il sorgente ServerCertificateValidationCallback, DangerousAcceptAnyServerCertificateValidator, ServerCertificateCustomValidationCallback, CheckCertificateRevocationList
Segreti scritti in chiaro Cercare Password=, ApiKey, Secret, Token, ConnectionString. Includere anche la storia del repository, non solo il sorgente attuale
Concatenazione SQL Cercare stringhe che contengono "SELECT, "INSERT, + e verificare che passino per Parameters.Add
Origine dei DLL caricati Con Process Monitor, filtrare per processo target e applicare Path ends with .dll e Result is NAME NOT FOUND. Si vedono le cartelle e l’ordine in cui cerca, quindi si può controllare che non venga consultata una cartella imprevista
Vulnerabilità delle dipendenze Eseguire dotnet list package --vulnerable --include-transitive
Segreti nei log Avviare l’app e cercare nel log emesso Bearer , Password, Authorization

La ricerca nel codice può essere fatta con rg (ripgrep) o con la ricerca di Visual Studio. Se preferisci eseguirla in blocco, puoi usare:

Get-ChildItem -Recurse -Include *.cs,*.vb,*.cpp,*.h,*.config,*.json |
    Select-String -Pattern 'ServerCertificateValidationCallback|DangerousAcceptAnyServerCertificateValidator|Password=|ApiKey' |
    Select-Object Path, LineNumber, Line

L’essenziale è registrare la verifica stessa. Annotare “cercato” e “0 risultati” ti permette di controllare solo le differenze al prossimo rilascio.

4. Lista di controllo pre-rilascio

Eccolo in un modulo che puoi utilizzare direttamente come modello per le revisioni e le decisioni di spedizione / non spedizione. Per una facile verifica, gli articoli minimi pre-release sono organizzati per categoria.

4.1. Privilegi e modello di esecuzione

Controlla l’articolo Fatto Note
L’avvio normale viene eseguito come asInvoker  
Le operazioni che richiedono diritti di amministratore sono isolate in un EXE / servizio separato  
Se viene utilizzato un servizio, il suo account non è più forte del necessario  
Le responsabilità sotto %ProgramFiles% e sotto i dati utente sono separate  

4.2. Distribuzione e firma

Controlla l’articolo Fatto Note
EXE / DLL / MSI / MSIX / updater sono firmati  
Le firme riportano la data e l’ora  
La scadenza e il rinnovo del certificato fanno parte del flusso di rilascio  
La verifica dell’hash / il rilevamento delle manomissioni per gli artefatti è definita  

4.3. Aggiornamenti

Controlla l’articolo Fatto Note
Gli aggiornamenti vengono recuperati nel corso di HTTPS  
La firma o l’hash vengono verificati dopo il download  
Il design rende difficile scambiare arbitrariamente la fonte di aggiornamento URL  
Esiste una politica di rollback o di nuovo tentativo per gli aggiornamenti non riusciti  

4.4. Segreti

Controlla l’articolo Fatto Note
Nessuna password, chiave API o stringa di connessione codificata nell’origine  
Nessun segreto nei file di configurazione in testo normale  
I segreti che devono essere archiviati localmente sono protetti con DPAPI / Credential Locker ecc.  
Ove possibile, vengono utilizzate l’autenticazione Windows o le credenziali dell’utente  

4.5. Comunicazione

Controlla l’articolo Fatto Note
Il traffico di produzione utilizza HTTPS  
Nessun DangerousAcceptAnyServerCertificateValidator o => true rimasto nelle build spedite  
Vengono presi in considerazione il controllo delle revoche e la convalida del nome host  
Nessun codice o impostazione presuppone che i certificati di sviluppo siano mescolati nella produzione  

4.6. Ingresso e accesso ai dati

Controlla l’articolo Fatto Note
SQL è parametrizzato  
Gli input da riga di comando, file, IPC e URI hanno limiti di dimensione e controlli di formato  
Le operazioni sul percorso vengono normalizzate e viene impedita l’escape root  
I messaggi di eccezione non elaborati non vengono visualizzati direttamente sullo schermo  

4.7. DLLs e l’ambiente di esecuzione

Controlla l’articolo Fatto Note
DLL le posizioni di carico sono esplicite  
L’ordine di ricerca è controllato tramite SetDefaultDllDirectories / AddDllDirectory ecc.  
Nessun DLL caricamento rimasto nella directory corrente o PATH  
È compreso il set completo di file necessari per il caricamento dinamico nei siti di distribuzione  

4.8. Registrazione e operazioni

Controlla l’articolo Fatto Note
Nessun token, password o PII nei log  
I registri interni e i messaggi rivolti agli utenti sono separati  
Le autorizzazioni sulle posizioni dump / trace / log storage sono state riviste  
SDK e lo stato dell’aggiornamento delle dipendenze è in fase di controllo  

5. Anti-pattern comuni

Ciò che vediamo più spesso nella pratica sono ipotesi come queste.

5.1. “È uno strumento interno, quindi va bene”

Gli strumenti interni devono ancora far fronte a file danneggiati, errori dell’operatore, dispositivi personali, cartelle condivise, DLLs obsoleti e impostazioni di autorizzazione sciatte. Non essere esposti a Internet non cancella la superficie di attacco.

5.2. “È HTTPS, quindi è sicuro”

HTTPS è importante, ma la disabilitazione della convalida del certificato svuota gran parte del suo significato. E per la distribuzione degli aggiornamenti, non è necessaria solo HTTPS ma anche la verifica dell’autenticità dell’artefatto.

5.3. “È crittografato, quindi è sicuro”

Senza stabilire dove risiede la chiave di decrittazione, chi può decrittografarla e i confini dell’utente e della macchina, la crittografia da sola non è sufficiente. In particolare, l’utilizzo di un valore protetto da LocalMachine come se fosse “un segreto per utente” crea confusione in seguito.

5.4. “Più registri significano indagini più semplici”

Se i registri sono voluminosi ma al loro interno vengono riversati token e informazioni personali, i registri stessi diventano l’incidente. Se vuoi la diagnosticabilità, decidi prima cosa mantenere e cosa oscurare.

5.5. “Basta eseguirlo come amministratore e il problema scompare”

Facile all’inizio, poi fa male in UAC, distribuzione, supporto, limiti di privilegi, caricamento DLL e posizioni dei file storage, più o meno in quest’ordine. Il privilegio minimo è più stabile nel lungo periodo.

6. Priorità approssimative

Se fare tutto in una volta è troppo pesante, le priorità sono più o meno queste.

  1. Verifica i privilegi di amministratore Innanzitutto, smetti di usare abitualmente requireAdministrator.
  2. Firma e timestamp Metti in ordine l’affidabilità dei tuoi artefatti.
  3. Evacuare i segreti Ottieni segreti dal codice sorgente e dalla configurazione in testo normale.
  4. Correzione HTTPS + convalida del certificato Rimuovi la famiglia => true dalle build spedite.
  5. Rivedi SQL / file / IPC input Riduci la concatenazione di stringhe e l’input non convalidato.
  6. Blocca DLL caricamento Arresta i caricamenti solo nome e la dipendenza PATH.
  7. Maschera i log Assicurati che i log non diventino un disastro secondario durante un incidente.
  8. Rendi routine gli aggiornamenti delle dipendenze Inserisci l’assegno in ogni versione.

In questo ordine, puoi procedere nello spirito di “tappare prima i buchi ovviamente pericolosi”.

Prima di introdurre prodotti speciali o framework di grandi dimensioni, la sicurezza delle app Windows cambia notevolmente semplicemente mettendo in ordine queste sette cose: privilegi, firma, segreti, comunicazione, input, DLLs e registrazione.

La barra minima, una riga ciascuna:

  • Non eseguire l’intera app con privilegi di amministratore
  • Firma i tuoi artefatti e aggiornamenti, con timestamp
  • Mantieni i segreti fuori dal codice sorgente e dalla configurazione in chiaro
  • Utilizza HTTPS e non interrompere la convalida del certificato
  • Non fidarti dell’input esterno: SQL, file, IPC e il resto
  • Non lasciare mai ambigue le posizioni di carico di DLL
  • Mantieni i segreti lontani dai registri
  • Non trascurare le tue dipendenze

La sicurezza è un argomento vasto, ma non è necessario occuparsene tutto fin dal primo giorno. L’unico minimo che vale la pena stabilire molto presto, tuttavia, è questo: non pubblicare mai valori predefiniti pericolosi così come sono.

8. Riferimenti

Articoli recenti con gli stessi tag per approfondire argomenti vicini.

Queste pagine collocano l’argomento in un contesto più ampio di servizi e decisioni.

L’articolo è direttamente collegato ai servizi seguenti.

Sviluppo di applicazioni Windows

La revisione completa di un'applicazione Windows (progettazione dei privilegi, metodo di distribuzione, meccanismo di aggiornamento e progettazione della registrazione) è una soluzione naturale per il nostro servizio di sviluppo di applicazioni Windows.

Domande frequenti

Domande che ricorrono nelle consulenze sull’argomento dell’articolo.

La mia app Windows deve essere eseguita con privilegi di amministratore?
Per impostazione predefinita, no. Le normali app dell'UI dovrebbero essere eseguite come asInvoker e solo le operazioni che necessitano effettivamente dei diritti di amministratore dovrebbero essere suddivise in un EXE separato o in un servizio elevato solo per quei momenti. L'esecuzione dell'intera app con privilegi elevati significa che bug, sostituzioni DLL, file di configurazione letti erroneamente e input esterno non convalidato vengono tutti eseguiti con quei privilegi avanzati. "È più semplice se viene eseguito solo come amministratore" ritorna quasi sempre a incidere sul comportamento, sulla distribuzione, sul supporto e sui limiti dei privilegi di UAC: il privilegio minimo è più stabile nel lungo periodo.
In che modo un'app Windows dovrebbe archiviare segreti come chiavi API e stringhe di connessione?
Tenerli prima lontani dal codice sorgente e dai file di configurazione in chiaro. Per i segreti crittografati inattivi localmente nelle app Win32/.NET, utilizzare DPAPI tramite ProtectedData; per le app desktop in pacchetto, considera Credential Locker; e laddove la destinazione supporti l'autenticazione Windows, preferisci non fare in modo che l'app contenga alcuna password. La crittografia da sola non è sufficiente: decidi nella progettazione chi può decrittografare, poiché DataProtectionScope.CurrentUser e LocalMachine significano cose molto diverse, ed evita di tenere la chiave di decrittografia accanto al testo cifrato.
È mai necessario OK disabilitare la convalida del certificato HTTPS in un'app spedita?
No: non saltare mai incondizionatamente la convalida del certificato. Codice come ServerCertificateValidationCallback che restituisce true o DangerousAcceptAnyServerCertificateValidator consente effettivamente la connessione indipendentemente da chi si connette, svuotando la maggior parte di ciò che fornisce HTTPS. I bypass aggiunti "solo per lo sviluppo" sono esattamente ciò che tende a rimanere intatto, quindi eliminali tramite condizioni di build o configurazione e, se hai davvero bisogno di un'eccezione di convalida, limitala a host e certificati specifici. Tieni presente anche il controllo della revoca.
Perché DLL sta caricando un problema di sicurezza nelle app Windows?
Il caricamento di un DLL solo per nome, come LoadLibrary("foo.dll"), consente all'ordine di ricerca di decidere da dove proviene DLL, che può prelevare un DLL da una posizione non prevista come la directory corrente o PATH. Le mitigazioni stabilite consistono nello specificare percorsi assoluti ove possibile, chiamare SetDefaultDllDirectories con LOAD_LIBRARY_SEARCH_DEFAULT_DIRS all'inizio dell'inizializzazione del processo e registrare posizioni aggiuntive esplicitamente con AddDllDirectory. Oltre alla sicurezza, ciò impedisce anche errori silenziosi quando una directory di lavoro cambia o DLL di un altro prodotto appare in PATH presso la sede di un cliente.

Profilo dell’autore

Pagina di presentazione dell’autore dell’articolo.

Go Komura

Rappresentante di KomuraSoft LLC

Specializzato nello sviluppo di software Windows, nella consulenza tecnica e nell’analisi dei malfunzionamenti, soprattutto nei progetti con sistemi esistenti e guasti difficili da riprodurre.

Torna al blog