Eine Mindest-Sicherheitscheckliste für die Windows-Anwendungsentwicklung
· Go Komura · Windows-Entwicklung, Sicherheit, Design, C# / .NET, Win32
Excel-Version der Checkliste herunterladen
Der Inhalt dieser Datei entspricht der Checkliste vor der Veröffentlichung aus Kapitel 4 (8 Kategorien, 32 Punkte). Der einzige Unterschied ist, dass Eingabefelder für Status und Notes hinzugefügt wurden und dass sowohl Japanisch als auch Englisch in den beiden Arbeitsblättern Checklist-ja / Checklist-en enthalten sind. Es genügt, Kapitel 4 zum Mitlesen und die Excel-Version zum Verteilen als Review-Protokoll zu verwenden.
Bei der Sicherheit von Windows-Anwendungen wird das Thema schnell riesig. Zero Trust, EDR, SBOM (Software Bill of Materials, Software-Stückliste), Zertifikatsbetrieb, Schwachstellenmanagement. All das ist wichtig, aber in der Praxis gibt es davor etliche Grundlagen, die nicht fehlen sollten.
Besonders bei den folgenden Anwendungen bringt es mehr, zuerst die Lücken in den Grundlagen zu schließen, als sofort auf „fortgeschrittene Verteidigung“ zu setzen.
- WPF- / WinForms- / WinUI-Desktopanwendungen
- C++ / C# Win32-Anwendungen
- Gerätekopplung, Dateikopplung, DB-Verbindungen, intern verteilte Tools
- Business-Anwendungen mit automatischem Update-Mechanismus
- Konfigurationen mit Windows-Diensten oder Hilfs-EXEs
Bei der Windows-Anwendungsentwicklung ist es realistischer, zunächst keine offensichtlich gefährlichen Lücken zu belassen, als alles auf einmal perfekt zu machen. Hier werden die Mindestpunkte, die nicht fehlen sollten, in der Reihenfolge Design, Implementierung, Distribution und Betrieb in leicht überprüfbarer Form zusammengestellt.
1. Zuerst das Fazit
- Als Erstes sollten diese Punkte nicht fehlen: keine unnötigen Administratorrechte anfordern, signieren, Geheimnisse nicht im Klartext halten, die Zertifikatsprüfung nicht deaktivieren.
- Bei einer Windows-Anwendung wird das Distributionsgut selbst zur Angriffsfläche. Es ist sicherer, alles einzubeziehen: EXE / DLL / MSI / MSIX / automatische Update-Module.
ServerCertificateValidationCallback => true, Klartext-Verbindungszeichenfolgen, ein nachlässigesLoadLibrary("foo.dll")und SQL-Ausführung per Zeichenkettenverkettung sind Punkte, die selbst auf dem Mindestniveau vermieden werden sollten.- Wird Administratorrecht nur für einen Teil der Verarbeitung benötigt, ist es sicherer, nicht die gesamte Anwendung zu erhöhen, sondern nur diesen Teil in eine separate EXE oder einen Dienst auszulagern.
- Für unter Windows verteilte Anwendungen sollte Signatur + Zeitstempel die Grundannahme sein. Das schafft nicht nur Vertrauen bei den Nutzern, sondern erleichtert auch die Manipulationserkennung und die betriebliche Nachvollziehbarkeit.
- Bei ruhenden Geheimnissen wird je nach Verwendungszweck zwischen DPAPI / ProtectedData und dem Credential Locker gewählt. Zumindest der Zustand, sie im Klartext in
appsettings.jsonabzulegen, sollte verlassen werden. - Mehr Logging ist nicht automatisch besser. Werden Token, Passwörter, Verbindungszeichenfolgen, personenbezogene Daten oder vollständige Request-Bodys unverändert belassen, wird das Log selbst zum Hauptdarsteller des Vorfalls.
Bei der Mindestsicherheit geht es weniger darum, besondere Funktionen hinzuzufügen, als vielmehr darum, keine gefährlichen Standardeinstellungen und keine nachlässige Implementierung zu belassen.
2. Geltungsbereich dieses Artikels und die Bedeutung von „Mindestmaß“
2.1. Erfasster Bereich
Dieser Artikel geht von folgenden Windows-Anwendungen aus.
- WPF- / WinForms- / WinUI-Desktopanwendungen
- C++ / C# Win32-Anwendungen
- Intern verteilte Tools, Gerätekopplungs-Tools, Überwachungstools
- Konfigurationen mit Hilfs-EXEs, Windows-Diensten und Updatern
- Als EXE / MSI / MSIX verteilte Business-Software
„Mindestmaß“ meint hier nicht eine Endform, die ein Audit besteht, sondern Punkte, bei deren Fehlen es normalerweise zu einem Vorfall kommt.
Auch die Voraussetzungen der Codebeispiele werden hier festgelegt. Die C#-Beispiele gehen von .NET 8 oder neuer aus, die C++-Beispiele von der Win32-API. Unter .NET Framework 4.8 bleibt der Denkansatz derselbe, aber an manchen Stellen hat sich die empfohlene Schreibweise geändert. Das typische Beispiel ist der ServicePointManager-Bereich aus 3.6: In neuem Code wird stattdessen vorausgesetzt, IHttpClientFactory und HttpClient zu verwenden. Achten Sie beim erneuten Lesen älterer Codegenerationen genau darauf.
2.2. Nicht erfasst
Andererseits gibt es Themen, die außerhalb des Kerns dieses Artikels liegen.
- Unternehmensweites Zero-Trust-Design
- Der Gesamtbetrieb von EDR / SIEM / DLP / MDM
- Detailliertes Hardening von Kernel-Treibern
- Das komplette Neu-Entwerfen kryptografischer Verfahren
- Fortgeschrittene Bedrohungsanalyse oder forensische Verfahren
Mit anderen Worten: Es geht nicht um „riesige, unternehmensweite Sicherheitsmaßnahmen“, sondern um die Grundlinie, die Windows-Anwendungsentwickler vor der Veröffentlichung selbst aus dem Weg räumen können.
3. Die zuerst zu betrachtende Checkliste
Vor der Detaildiskussion steht hier eine Tabelle, die den gesamten Überblick zeigt. Allein damit lässt sich schon abschätzen, wo genauer hinzuschauen ist.
3.1. Das Gesamtbild
| Prüfpunkt | Mindestmaßnahme | Typisches Anti-Pattern |
|---|---|---|
| Ausführungsrechte | asInvoker als Standard, nur Vorgänge mit Rechteerhöhung auslagern |
Die gesamte Anwendung auf requireAdministrator setzen |
| Vertrauenswürdigkeit des Distributionsguts | EXE / DLL / MSI / MSIX codesignieren, mit Zeitstempel | Unsigniert ausliefern |
| Updates | Update-Quelle festlegen, Manipulation über HTTPS und Signaturprüfung erkennen | Nach HTTP-Download direkt überschreiben |
| Geheimnisse | Keine Geheimnisse im Quellcode oder in Klartextkonfiguration, DPAPI / Credential Locker o. Ä. verwenden | API-Schlüssel oder Verbindungszeichenfolgen im Klartext in Konfigurationsdateien |
| Kommunikation | HTTPS verwenden, Zertifikatsprüfung nicht deaktivieren | Zertifikatsprüfung mit return true dauerhaft überspringen |
| Externe Eingaben | SQL, Dateien, IPC, URI, CSV, JSON usw. vollständig validieren | Mit „ist ja nur ein internes Tool“ ungeprüft durchlassen |
| DLL-Laden | Absoluten Pfad, SetDefaultDllDirectories, sichere Suchreihenfolge verwenden |
LoadLibrary("foo.dll") dem aktuellen Verzeichnis überlassen |
| Logging | Token, Passwörter, PII maskieren, nutzerseitige Fehler getrennt ausgeben | Ausnahmedetails oder Verbindungszeichenfolgen unverändert anzeigen oder speichern |
| Abhängigkeiten | SDK, NuGet, VC++-Runtime, OSS-Abhängigkeiten laufend aktualisieren | Über Jahre hinweg fixieren und Schwachstelleninformationen nicht verfolgen |
3.2. Rechte grundsätzlich auf asInvoker setzen
Das ist der erste Punkt, den Sie bei einer Windows-Anwendung überprüfen sollten. Läuft die gesamte Anwendung mit Administratorrechten, werden Fehler, DLL-Austausch, fehlerhaft gelesene Konfigurationsdateien und unzureichend geprüfte externe Eingaben direkt mit diesen starken Rechten ausgeführt.
Die Grundregel lautet:
- Gewöhnliche UI-Anwendungen laufen als
asInvoker - Nur die Vorgänge, die Administratorrechte benötigen, werden in einen separaten Prozess oder Dienst ausgelagert
- Die Rechteerhöhung erfolgt nur für den jeweils nötigen Moment
- Auch die an Hilfs-EXEs oder Dienste übergebenen Eingaben werden validiert
Handelt es sich um eine Desktopanwendung, die normalerweise nur anzeigt und bearbeitet, und benötigen ausschließlich die Installation oder Änderungen an der Firewall-Konfiguration Administratorrechte, ist es sicherer, nur den Teil, der die Rechteerhöhung braucht, an einen Broker auszulagern, statt die gesamte Anwendung auf requireAdministrator zu setzen.
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
„Es ist einfacher, wenn es als Administrator läuft“ rächt sich meistens später. Läuft die Anwendung mit minimalen Rechten und werden nur die tatsächlich nötigen Vorgänge herausgelöst, wird der Wirkungsradius eines Vorfalls erheblich kleiner.
3.3. Binärdateien und Installer signieren
Unter Windows zählt die Vertrauenswürdigkeit des Distributionsguts. Was die Nutzer berühren, ist nicht der Quellcode, sondern die EXE, DLL, MSI, MSIX und der Updater. Bleiben diese unsigniert, leiden die betriebliche Nachvollziehbarkeit, die Manipulationserkennung und das Vertrauensgefühl bei der Verteilung gleichermaßen.
Zumindest diese Punkte sollten geprüft werden.
- EXE / DLL / MSI / MSIX signieren
- Nicht nur den Installer, sondern auch die für Updates verwendeten Hilfsbinärdateien signieren
- Zeitstempel hinzufügen
- Zertifikatsablauf und Erneuerungsverfahren in den Release-Ablauf aufnehmen
Besonders Signaturen ohne Zeitstempel bereiten bei der Prüfung nach Ablauf des Zertifikats leicht Probleme. Statt „signiert, also fertig“ ist es stabiler, Signatur + Zeitstempel fest in den Release-Ablauf aufzunehmen.
Wird MSIX verwendet, ist die Paketsignatur Voraussetzung. Auch bei MSI- / EXE-Verteilung sollten zumindest der Installer selbst und die wichtigsten ausführbaren Binärdateien signiert sein.
3.4. Update-Kanal festlegen und Manipulationserkennung einbauen
Bei einer heutigen Windows-Anwendung wird der Update-Kanal über die Lebenszeit hinweg länger genutzt als die Erstinstallation. Wird das hier nachlässig gehandhabt, wird der Updater zur schwächsten Stelle, egal wie sorgfältig der Hauptteil gebaut wurde.
Rund um Updates sollten mindestens diese fünf Punkte bedacht werden.
- Der Abruf der Update-Dateien setzt HTTPS voraus
- Die heruntergeladenen Update-Artefakte werden über Signatur oder Hash geprüft
- Die Update-Quell-URL kann nicht uneingeschränkt per Code oder Konfiguration ausgetauscht werden
- Auch das Update-Modul selbst wird signiert
- Rollback- oder Wiederherstellungsverfahren bei Fehlschlägen sind festgelegt
Lässt sich MSIX + App Installer einsetzen, kann der Update-Mechanismus stärker in Richtung des Betriebssystems verlagert werden. Wird dagegen ein eigener Updater betrieben, müssen sowohl die Sicherheit der Übertragung als auch die Echtheit des Distributionsguts geprüft werden. HTTPS allein schützt zwar den „Übertragungsweg“, garantiert aber nicht, dass „diese Datei wirklich das eigene veröffentlichte Werk ist“.
3.5. Geheimnisse nicht im Quellcode oder in Klartextkonfiguration ablegen
Das ist in der Praxis eine Stelle, an der wirklich häufig Vorfälle passieren. Mit „ist ja nur ein internes Tool“ oder „wir geben sowieso nur eine exe heraus“ landen Verbindungszeichenfolgen, API-Schlüssel, Anmeldeinformationen für Freigabeordner und feste Token gerne im Quellcode oder in Konfigurationsdateien.
Diese Ablageformen sollten mindestens vermieden werden.
- Direkt im Quellcode eingetragene API-Schlüssel
- Klartextpasswörter in
appsettings.jsonoderapp.config - Ins Repository eingecheckte Verbindungszeichenfolgen
- Designs, die Entschlüsselungsschlüssel und Chiffretext am selben Ort ablegen
- Feste Anmeldeinformationen, die für alle gemeinsam statt pro Nutzer gelten
Für eine Windows-Anwendung kommen realistischerweise ungefähr diese vier Optionen infrage.
- Windows-Anmeldeinformationen sollen gespeichert werden Bei einer packaged Desktop-App / im WinUI-Umfeld den Credential Locker in Betracht ziehen
- Geheimnisse sollen lokal verschlüsselt gespeichert werden
Bei Win32 / .NET DPAPI /
ProtectedDataverwenden - Das Ziel kann Windows-Authentifizierung oder integrierte Authentifizierung nutzen Wenn möglich, der Anwendung gar kein Passwort geben
- Geheimnisse lassen sich in der Cloud oder serverseitig verwalten Designs bevorzugen, die keine langlebigen Geheimnisse im Client einbetten
Unter C# ist es, zumindest so wie im Folgenden mit DPAPI, schon deutlich besser als eine Klartextspeicherung. Schreibt man Speichern und Laden als vollständigen Zyklus aus, sieht das so aus.
// C# / .NET 8. ProtectedData ist Windows-exklusiv;
// unter .NET wird dafür das NuGet-Paket System.Security.Cryptography.ProtectedData benötigt.
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
public static class SecretStore
{
// Wird auch beim Entschlüsseln benötigt. Funktioniert auch mit null, ist mit einem Wert aber sicherer.
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);
// Ließe sich auch als reine Byte-Folge ablegen; für eine Konfigurationsdatei wird hier jedoch Base64 verwendet.
File.WriteAllText(path, Convert.ToBase64String(ciphertext));
CryptographicOperations.ZeroMemory(plaintext);
}
public static string Load(string path)
{
byte[] ciphertext = Convert.FromBase64String(File.ReadAllText(path));
// Ohne denselben Benutzer und dieselbe Entropy wie beim Speichern kommt es zu einer CryptographicException.
byte[] plaintext = ProtectedData.Unprotect(
ciphertext,
Entropy,
DataProtectionScope.CurrentUser);
try
{
return Encoding.UTF8.GetString(plaintext);
}
finally
{
CryptographicOperations.ZeroMemory(plaintext);
}
}
}
Auf der Aufruferseite sieht das dann so aus.
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);
Wichtig ist hier nicht „verschlüsselt, also sicher“, sondern im Design festzulegen, wer entschlüsseln kann.
Ob CurrentUser oder LocalMachine gewählt wird, ändert die Bedeutung erheblich. CurrentUser kann nur der Benutzer entschlüsseln, der gespeichert hat; LocalMachine kann jeder auf derselben Maschine entschlüsseln. Wird die Anwendung als Dienst unter einem anderen Konto ausgeführt oder gibt es einen Betrieb mit wechselnden Benutzern, muss das vorher festgelegt werden – sonst gerät man später entweder an „lässt sich nicht lesen“ oder an „lässt sich lesen, obwohl es nicht sollte“.
Da DPAPI seinen Schlüssel im Benutzerprofil hält, ist in der Dokumentation außerdem festgehalten, dass die Entschlüsselung fehlschlägt, wenn das Profil nicht geladen ist (etwa während einer Impersonation). Wird eine Nutzung aus einem Dienst heraus erwogen, sollte auch dieser Punkt geprüft werden.
Bei SQL-Server-Verbindungen kann in On-Premises-Umgebungen die Windows-Authentifizierung oft die erste Wahl sein.
Müssen unbedingt Anmeldeinformationen in die Verbindungszeichenfolge aufgenommen werden, ist es zumindest sicherer, Persist Security Info=False beizubehalten und sie nicht dauerhaft in einer Klartextkonfigurationsdatei liegen zu lassen.
3.6. Kommunikation setzt HTTPS voraus – die Zertifikatsprüfung nicht abschalten
Ein Schlupfloch, das nur „für die Entwicklung“ gedacht war, bleibt unverändert in Produktion. Vorfälle rund um die Kommunikation folgen meist diesem Muster.
Besonders leicht bleiben diese Art von Code oder Einstellungen im ausgelieferten Produkt hängen.
ServicePointManager.ServerCertificateValidationCallback += ... => trueHttpClientHandler.DangerousAcceptAnyServerCertificateValidator- Ausliefern mit deaktivierter Zertifikatswiderrufsprüfung
- Code, der von einem selbstsignierten Entwicklungszertifikat ausgeht, bleibt in Produktion
Die Mindestrichtlinie ist einfach.
- Produktivkommunikation läuft über HTTPS
- Die Zertifikatsprüfung wird nicht dauerhaft übersprungen
- Ist ausnahmsweise eine gelockerte Prüfung nötig, wird sie auf den betroffenen Host und das betroffene Zertifikat beschränkt
- Entwicklungs-Umgehungscode wird über Build-Bedingungen oder Konfiguration zuverlässig entfernt
- Unter .NET wird auch an die Widerrufsprüfung gedacht
Das schlechte Beispiel sieht meistens so aus.
ServicePointManager.ServerCertificateValidationCallback +=
(_, _, _, _) => true;
Das wirkt auf den ersten Blick bequem, verhält sich aber nahezu wie „diese HTTPS-Verbindung wird durchgelassen, egal mit wem sie spricht“. Wird die Zertifikatsprüfung entfernt, ist der Inhalt trotz HTTPS weitgehend ausgehöhlt.
Die Schreibweise hängt davon ab, welches .NET vorausgesetzt wird
Eine globale Einstellung am ServicePointManager zu setzen ist eine Schreibweise aus der Zeit des .NET Framework. In neuem Code ist es naheliegender, HttpClient über IHttpClientFactory zu beziehen und, falls TLS-bezogene Einstellungen nötig sind, diese auf SocketsHttpHandler beziehungsweise HttpClientHandler zu legen.
Es ist jedoch gefährlich, das mit „ist eh eine alte API, die wirkt schon nicht mehr“ zu ignorieren. In der Microsoft-Dokumentation steht, dass ServicePointManager.ServerCertificateValidationCallback ab .NET 9 auf RemoteCertificateValidationCallback von SocketsHttpHandler.SslOptions abgebildet wird. Mit anderen Worten: Ein einziges => true irgendwo im Code kann auch die Kommunikation von HttpClient durchwinken.
Soll die Prüfung ausnahmsweise gelockert werden, geschieht das nicht über eine prozessweit wirkende globale Einstellung, sondern beschränkt auf genau diesen Handler.
// C# / .NET 8. Beispiel, das nur einen bestimmten Host und ein bestimmtes Zertifikat als Ausnahme behandelt.
// Auch wenn für Entwicklungszwecke gelockert wird: Ohne Beschränkung des Ziels ist das nichts anderes als ein globales Abschalten.
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
// Fingerabdruck des Zielzertifikats. Kann auch aus der Konfiguration gelesen werden.
const string ExpectedThumbprint = "2B0C4E6A8D1F3B5D7F9A1C3E5A7C9E1B3D5F7A91";
var handler = new HttpClientHandler
{
CheckCertificateRevocationList = true, // Prüft den Widerruf. Standard ist false
ServerCertificateCustomValidationCallback = (request, certificate, chain, errors) =>
{
if (errors == SslPolicyErrors.None)
{
return true;
}
// Akzeptiert wird nur der eine Fall "dieses Gerät vertraut der internen CA nicht".
// Nicht akzeptiert werden ein fehlendes Zertifikat oder eine abweichende Hostname
if (errors != SslPolicyErrors.RemoteCertificateChainErrors)
{
return false;
}
// Gegenstelle und Zertifikat festlegen
if (request.RequestUri?.Host != "device.internal.example"
|| certificate is null
|| !string.Equals(certificate.Thumbprint, ExpectedThumbprint,
StringComparison.OrdinalIgnoreCase))
{
return false;
}
// Auch beim gepinnten Zertifikat werden Ablauf und Widerruf nicht akzeptiert.
// Würde das akzeptiert, bliebe der Pfad offen, ein Zertifikat weiterzuverwenden,
// das wegen eines kompromittierten Schlüssels widerrufen wurde
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 wird als Konstante oder aus der Konfiguration mit dem Fingerabdruck des Zielzertifikats belegt.
Wichtig ist hier, errors und chain.ChainStatus nicht „zu ignorieren“. Wird allein deshalb, weil der Fingerabdruck übereinstimmt, true zurückgegeben, lässt das Zertifikat auch dann weiter durch, wenn es abgelaufen oder wegen eines kompromittierten Schlüssels widerrufen wurde. Pinning bedeutet „nur genau diesem einen Zertifikat vertrauen“, nicht „diesem einen Zertifikat vertrauen, egal was passiert“. Der obige Code lässt nur UntrustedRoot und PartialChain durch (das heißt, die interne CA ist auf diesem Gerät nicht installiert); NotTimeValid (abgelaufen) und Revoked (widerrufen) werden weiterhin abgelehnt.
Um den Widerruf zu prüfen, ist CheckCertificateRevocationList = true nötig (der Standard ist false, dann wird der Widerruf nicht geprüft). Veröffentlicht die interne CA umgekehrt weder eine CRL noch OCSP, wird mit RevocationStatusUnknown abgelehnt. Das ist das korrekte Verhalten. Steht kein Mittel zur Widerrufsprüfung zur Verfügung, sollte entweder die Gültigkeitsdauer des Zertifikats verkürzt oder vorab ein Weg geschaffen werden, den gepinnten Wert neu zu verteilen. Am gefährlichsten ist es, „ein langlebiges Zertifikat zu pinnen, ohne es widerrufen zu können“.
3.7. Alle externen Eingaben als „nicht vertrauenswürdige Eingabe“ behandeln
Windows-Anwendungen sind keine Webanwendungen, deshalb wird die Eingabevalidierung leicht nachlässig. In Wirklichkeit gibt es aber deutlich mehr Einfallstore für externe Eingaben, als man denkt.
- Dateipfade
- CSV / Excel / JSON / XML
- Kommandozeilenargumente
- Named Pipes / Sockets / COM / RPC / gRPC
- An die DB übergebene Zeichenfolgen
- Registrierungswerte
- Die Zwischenablage
- URLs / Deep Links
- Von externen Geräten oder SDKs zurückgegebene Daten
Besonders diese drei sollten mindestens nicht fehlen.
- SQL immer parametrisieren SQL nicht per Zeichenkettenverkettung zusammensetzen.
- Dateipfade vor der Verwendung normalisieren Einen von Nutzern angegebenen Pfad nicht unverändert zum Löschen, Überschreiben oder Entpacken verwenden.
- Beim Einlesen externer Dateien Größenobergrenze und Formatprüfung einbauen „Ließ sich öffnen, also sicher“ gilt nicht.
Am SQL-Beispiel gezeigt, sollte Folgendes vermieden werden.
var sql = "SELECT * FROM Users WHERE Name = '" + userName + "'";
Mindestens sollte es so aussehen.
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;
„Ist ja ein internes Tool, also ist die Eingabe vertrauenswürdig“ ist eine ziemlich gefährliche Annahme. In der Realität kommen defekte CSVs, unerwartete Dateinamen, veraltete DB-Daten, Tippfehler von Betreibern und von anderen Tools geschriebenes halbfertiges JSON ganz normal herein.
3.8. Die Herkunft geladener DLLs nicht im Unklaren lassen
Das ist eine typisch windowstypische Falle.
Lässt man eine DLL wie bei LoadLibrary("foo.dll") nur über den Namen laden, kann je nach Suchreihenfolge eine DLL aus einem unbeabsichtigten Ort geladen werden.
Was zu tun ist, steht fest.
- Wenn möglich, den absoluten Pfad der DLL angeben
SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS)früh setzen- Mit
AddDllDirectoryexplizit zusätzliche Suchorte hinzufügen - Designs vermeiden, die das Ergebnis von
SearchPathdirekt anLoadLibraryweiterreichen - Sich nicht vollständig auf den Safe-DLL-Search-Modus verlassen
Bei nativem Code beispielsweise ist es ein starkes Muster, Folgendes früh bei der Prozessinitialisierung einzubauen.
SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);
Und dann nur die tatsächlich benötigten zusätzlichen Verzeichnisse mit AddDllDirectory registrieren.
Weil das „normalerweise funktioniert“, bleibt dieser Punkt leicht liegen – ändert sich beim Kunden aber das Arbeitsverzeichnis, oder gerät die DLL eines anderen Produkts in den PATH, bricht es lautlos. Das wirkt sich nicht nur auf die Sicherheit aus, sondern erheblich auch als Vorbeugung gegen Störungen.
3.9. Keine Geheimnisse in Logs und Ausnahmen ausgeben
Mehr Logs für die Störungsanalyse anzulegen ist wichtig. Logs werden aber auch leicht zum Friedhof für Geheimnisse.
Rund ums Logging sollten mindestens diese Punkte überprüft werden.
- Passwörter, Bearer-Token, API-Schlüssel nicht ins Log schreiben
- Verbindungszeichenfolgen nicht vollständig ausgeben
- Personenbezogene Daten und Inhalte von Geschäftsdaten maskieren
- Ausnahmedetails zwischen nutzerseitiger Anzeige und internem Log trennen
- Debug-PII-Logging nicht in Produktion aktivieren
- Zugriffsrechte auf Speicherorte für Dumps und Traces überprüfen
Aktuelles .NET erleichtert eine Redaction-first-Struktur zunehmend. Mindestens sollte „alles in einen String verwandeln und einfach loggen“ aufgegeben werden.
Ein paar klassische Fehler:
- Den kompletten HTTP-Request-/Response-Body speichern
- Bei fehlgeschlagener Authentifizierung den Token oder alle Header ausgeben
- Ausnahmemeldungen unverändert in einer MessageBox anzeigen
- Sämtliche vertraulichen Logs in ein Wartungs-ZIP packen
Die Fehleranzeige wird zum Beispiel so getrennt.
- Für Nutzer: „Die Verbindung zum Server ist fehlgeschlagen. Bitte überprüfen Sie die Netzwerkeinstellungen und die URL.“
- Internes Log: Zielhost, TLS-Fehlertyp, Korrelations-ID, Stacktrace, Anzahl der Wiederholungsversuche
Allein diese Trennung verbessert die Balance zwischen Informationsschutz und Untersuchbarkeit erheblich.
3.10. Abhängige Bibliotheken und Entwicklungswerkzeuge nicht vernachlässigen
Der letzte Punkt ist unscheinbar, wirkt aber stark. Auch wenn die Anwendung selbst sorgfältig gebaut ist: Läuft sie auf einer veralteten Runtime oder mit Abhängigkeiten mit bekannten Schwachstellen, gibt der Boden nach.
Die zu prüfenden Punkte sind selbst nicht zahlreich.
- .NET SDK / Runtime auf einer unterstützten Version halten
- Updates von NuGet- / OSS-Abhängigkeiten regelmäßig prüfen
- Bei C++ die Versionsverwaltung von Runtime-Redistributables und externen DLLs betreiben
- Die Prüfung von Schwachstelleninformationen in die Vor-Release-Kontrolle aufnehmen
- Smoke-Tests bereithalten, damit Abhängigkeits-Updates nichts unbemerkt kaputt machen
Am gefährlichsten ist hier die Haltung „das machen wir später gesammelt“. Bleibt das ein halbes oder ein ganzes Jahr liegen, wird das Update-Delta so groß, dass die Sicherheitsarbeit selbst zu einem Großprojekt wird.
3.11. Wie sich die einzelnen Punkte prüfen lassen
Eine Checkliste funktioniert erst zusammen mit einer Prüfmethode. Für die Punkte aus 3.2 bis 3.10 wird hier zusammengestellt, was sich vor der Veröffentlichung tatsächlich ausführen lässt.
| Was geprüft werden soll | Prüfmethode |
|---|---|
| Wird keine Rechteerhöhung angefordert? | Im Manifest der Anwendung den Wert von requestedExecutionLevel ansehen. Im Quellcode ist das app.manifest, beim Distributionsgut lässt sich das mit Sysinternals Sigcheck oder einem Ressourceneditor prüfen |
| Signatur und Zeitstempel | In PowerShell Get-AuthenticodeSignature .\app.exe ausführen und prüfen, ob Status gleich Valid ist und TimeStamperCertificate gesetzt ist. Auch mitgelieferte DLLs und den Updater einzeln prüfen |
| Wurde die Zertifikatsprüfung abgeschaltet? | Den gesamten Quellcode nach ServerCertificateValidationCallback, DangerousAcceptAnyServerCertificateValidator, ServerCertificateCustomValidationCallback und CheckCertificateRevocationList durchsuchen |
| Direkt eingetragene Geheimnisse | Nach Password=, ApiKey, Secret, Token, ConnectionString suchen. Nicht nur der aktuelle Quellcode, sondern auch die Repository-Historie sollte einbezogen werden |
| Aufbau von SQL | Zeichenkettenverkettung mit "SELECT, "INSERT, + suchen und prüfen, ob dabei Parameters.Add verwendet wird |
| Herkunft geladener DLLs | Mit Process Monitor auf den Zielprozess einschränken und die Filter Path ends with .dll sowie Result is NAME NOT FOUND setzen. So wird sichtbar, wo und in welcher Reihenfolge gesucht wurde, und lässt sich prüfen, ob unbeabsichtigte Ordner betroffen sind |
| Bekannte Schwachstellen in Abhängigkeiten | dotnet list package --vulnerable --include-transitive ausführen |
| Werden im Log Geheimnisse ausgegeben? | Die Anwendung einmal ausführen und das ausgegebene Log nach Bearer , Password, Authorization durchsuchen |
Für die Codesuche eignen sich sowohl rg (ripgrep) als auch die Suche in Visual Studio. Für eine gesammelte Suche bietet sich diese Form an.
Get-ChildItem -Recurse -Include *.cs,*.vb,*.cpp,*.h,*.config,*.json |
Select-String -Pattern 'ServerCertificateValidationCallback|DangerousAcceptAnyServerCertificateValidator|Password=|ApiKey' |
Select-Object Path, LineNumber, Line
Wichtig ist, die Prüfung selbst zu dokumentieren. Wird festgehalten, dass gesucht wurde und dass es „0 Treffer“ gab, genügt es bei der nächsten Veröffentlichung, nur die Differenz zu betrachten.
4. Checkliste vor der Veröffentlichung
Diese Checkliste lässt sich direkt als Vorlage für Reviews und Auslieferungsentscheidungen verwenden. Damit sie sich leicht in Tabellenform prüfen lässt, werden die vor der Veröffentlichung mindestens zu betrachtenden Punkte nach Kategorien geordnet aufgelistet.
4.1. Rechte, Ausführungsart
| Prüfpunkt | Geprüft | Notiz |
|---|---|---|
Der normale Start läuft als asInvoker |
□ | |
| Vorgänge, die Administratorrechte benötigen, sind in eine separate EXE / einen Dienst ausgelagert | □ | |
| Wird ein Dienst verwendet, läuft er nicht mit einem unnötig starken Ausführungskonto | □ | |
Die Zuständigkeiten unter %ProgramFiles% und unter den Benutzerdaten sind getrennt |
□ |
4.2. Distribution, Signatur
| Prüfpunkt | Geprüft | Notiz |
|---|---|---|
| EXE / DLL / MSI / MSIX / Updater sind signiert | □ | |
| Die Signatur trägt einen Zeitstempel | □ | |
| Zertifikatsablauf und Erneuerungsverfahren sind Teil des Release-Ablaufs | □ | |
| Hash-Prüfung bzw. Manipulationserkennung für das Distributionsgut ist festgelegt | □ |
4.3. Updates
| Prüfpunkt | Geprüft | Notiz |
|---|---|---|
| Der Update-Abruf erfolgt über HTTPS | □ | |
| Nach dem Download werden Signatur oder Hash geprüft | □ | |
| Das Design erschwert ein beliebiges Austauschen der Update-Quell-URL | □ | |
| Es gibt eine Rollback- oder Wiederholungsstrategie bei fehlgeschlagenen Updates | □ |
4.4. Geheimnisse
| Prüfpunkt | Geprüft | Notiz |
|---|---|---|
| Passwörter, API-Schlüssel und Verbindungszeichenfolgen sind nicht direkt im Quellcode eingetragen | □ | |
| In Klartextkonfigurationsdateien sind keine Geheimnisse abgelegt | □ | |
| Lokal zu speichernde Geheimnisse sind mit DPAPI / Credential Locker o. Ä. geschützt | □ | |
| Wo möglich, wird auf Windows-Authentifizierung oder Benutzeranmeldeinformationen gesetzt | □ |
4.5. Kommunikation
| Prüfpunkt | Geprüft | Notiz |
|---|---|---|
| Die Produktivkommunikation nutzt HTTPS | □ | |
DangerousAcceptAnyServerCertificateValidator oder => true sind nicht im ausgelieferten Produkt enthalten |
□ | |
| Widerrufsprüfung und Hostnamensvalidierung sind berücksichtigt | □ | |
| Kein Code oder keine Einstellung, die von Entwicklungszertifikaten ausgeht, ist in Produktion vermischt | □ |
4.6. Eingaben, Datenzugriff
| Prüfpunkt | Geprüft | Notiz |
|---|---|---|
| SQL ist parametrisiert | □ | |
| Eingaben über Kommandozeile, Dateien, IPC, URI usw. haben Obergrenzen und Formatprüfungen | □ | |
| Pfadoperationen werden normalisiert, um ein Verlassen des Basisverzeichnisses zu verhindern | □ | |
| Ausnahmemeldungen werden nicht unverändert auf dem Bildschirm angezeigt | □ |
4.7. DLLs und Ausführungsumgebung
| Prüfpunkt | Geprüft | Notiz |
|---|---|---|
| Die Herkunft geladener DLLs ist explizit angegeben | □ | |
Die Suchreihenfolge wird über SetDefaultDllDirectories / AddDllDirectory o. Ä. gesteuert |
□ | |
| Kein DLL-Laden ist dem aktuellen Verzeichnis oder dem PATH überlassen | □ | |
| Die für dynamisches Laden am Zielort benötigten Dateien sind bekannt | □ |
4.8. Logging, Betrieb
| Prüfpunkt | Geprüft | Notiz |
|---|---|---|
| Es werden keine Token, Passwörter oder PII geloggt | □ | |
| Internes Log und nutzerseitige Meldungen sind getrennt | □ | |
| Die Zugriffsrechte auf Speicherorte für Dumps / Traces / Logs sind überprüft | □ | |
| Der Update-Status von SDK und Abhängigkeiten wird kontrolliert | □ |
5. Häufige Fehlannahmen
In der Praxis begegnen einem meist Annahmen wie diese.
5.1. „Ist ja ein internes Tool, also ist alles gut“
Auch bei internen Tools kommen defekte Dateien, Fehlbedienung, mitgebrachte Geräte, Freigabeordner, veraltete DLLs und nachlässige Rechtekonfigurationen ganz normal vor. Fehlt die Veröffentlichung im Internet, verschwindet die Angriffsfläche dadurch nicht.
5.2. „Ist ja HTTPS, also sicher“
HTTPS ist wichtig, aber wird die Zertifikatsprüfung deaktiviert, verliert es einen Großteil seiner Bedeutung. Zudem braucht die Update-Verteilung nicht nur HTTPS, sondern auch die Prüfung der Echtheit des Distributionsguts.
5.3. „Ist ja verschlüsselt, also sicher“
Ohne Klärung, wo der Entschlüsselungsschlüssel liegt, wer entschlüsseln darf und wo Benutzer- und Maschinengrenzen verlaufen, reicht Verschlüsselung allein nicht aus.
Besonders wenn ein mit LocalMachine geschützter Wert fälschlich als „Geheimnis pro Benutzer“ verwendet wird, führt das später zu Verwirrung.
5.4. „Mehr Logs bedeuten bessere Untersuchbarkeit“
Sind die Logs zwar umfangreich, laufen aber Token und personenbezogene Daten ungefiltert mit, wird das selbst zum Vorfall. Wer Untersuchbarkeit will, sollte zuerst festlegen, was aufbewahrt und was verborgen wird.
5.5. „Als Administrator laufen lassen löst es“
Am Anfang ist das bequem, später wird es meist bei UAC, Distribution, Support, Rechtegrenzen, DLL-Laden und Dateispeicherort mühsam. Minimale Rechte sind langfristig stabiler.
6. Grobe Priorisierung
Wenn alles auf einmal zu schwer ist, sieht die Priorisierung ungefähr so aus.
Das Ordnungskriterium ist die Kombination aus der Schadenshöhe im Vorfall und den niedrigen Kosten der Behebung. Kapitel 3 ist nach dem Designfluss „Rechte → Distribution → Implementierung → Betrieb“ geordnet, diese Liste dagegen nach „was am gefährlichsten ist“, entspricht also nicht der Kapitelreihenfolge. Der jeweils zugehörige Abschnitt ist mit angegeben.
- Administratorrechte überprüfen (3.2)
Zunächst die dauerhafte Nutzung von
requireAdministratorbeenden. Der Schadensbereich ändert sich um eine ganze Stufe, während die Designänderung oft klein bleibt. - Signatur und Zeitstempel (3.3) Die Vertrauenswürdigkeit des Distributionsguts herstellen. Lässt sich einfach in den Ablauf einbauen; wird es später nachgeholt, ist eine erneute Verteilung nötig.
- Geheimnisse in Sicherheit bringen (3.5) Geheimnisse aus Quellcode und Klartextkonfiguration entfernen. Der Schaden im Leck-Fall ist groß, und nach dem Leck lässt er sich nicht mehr rückgängig machen.
- HTTPS + Zertifikatsprüfung korrigieren (3.6)
Die
=> true-Familie aus dem ausgelieferten Produkt entfernen. Oft genügt schon das Löschen zur Behebung; bleibt es liegen, ist die gesamte Kommunikation nicht mehr vertrauenswürdig. - SQL- / Datei- / IPC-Eingaben überprüfen (3.7) Zeichenkettenverkettung und ungeprüfte Eingaben reduzieren. Es gibt viele Stellen, aber sie lassen sich einzeln beheben.
- DLL-Laden fixieren (3.8) Das reine Namens-Laden und die Abhängigkeit vom PATH beenden. Das betrifft nur einen Teil der Startverarbeitung und wirkt zugleich als Störungsvorbeugung.
- Logs maskieren (3.9) Dafür sorgen, dass Logs im Vorfall nicht zum Folgeschaden werden. Da es viele Ausgabestellen gibt, dauert das seine Zeit.
- Abhängigkeits-Updates zur Routine machen (3.10) Einen Ablauf etablieren, der bei jeder Veröffentlichung geprüft wird. Das ist nicht mit einem Mal erledigt – die eigentliche Arbeit besteht darin, daraus einen festen Prozess zu machen.
Der Update-Kanal (3.4) fehlt in dieser Reihenfolge, weil nicht jede Anwendung ein automatisches Update besitzt. Ist eines vorhanden, betrachten Sie ihn mit derselben Priorität wie Punkt 2, die Signatur. Das Update-Modul steht in der Position, den Hauptteil überschreiben zu können, ist dabei aber schwächer abgesichert als dieser.
Mit dieser Reihenfolge lässt sich im Sinne von „zuerst die offensichtlich gefährlichen Lücken schließen“ gut vorgehen.
7. Zusammenfassung
Bevor spezielle Produkte oder ein riesiges System eingeführt werden, verändert sich die Sicherheit der Windows-Anwendungsentwicklung schon erheblich, wenn allein diese sieben Punkte in Ordnung gebracht werden: Rechte, Signatur, Geheimnisse, Kommunikation, Eingaben, DLLs und Logging.
Auf je einen Satz gebracht, lautet das Mindestniveau so.
- Die gesamte Anwendung nicht mit Administratorrechten laufen lassen
- Distributionsgut und Updates signieren, mit Zeitstempel
- Geheimnisse nicht im Quellcode oder in Klartextkonfiguration ablegen
- Auch bei HTTPS die Zertifikatsprüfung nicht abschalten
- Externen Eingaben wie SQL, Dateien, IPC usw. nicht vertrauen
- Die Herkunft geladener DLLs nicht im Unklaren lassen
- Keine Geheimnisse im Log ausgeben
- Abhängige Bibliotheken nicht vernachlässigen
Sicherheit ist ein weites Feld, aber es muss nicht von Anfang an alles erledigt werden. Allein dieses eine Mindestmaß – gefährliche Standardeinstellungen nicht unverändert ausliefern – lohnt sich aber, schon sehr früh sicherzustellen.
8. Referenzen
- 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 – dazu, dass es sich um eine reine Windows-Klasse handelt, sowie zum Hinweis für den Fall eines nicht geladenen Profils.
- ServicePointManager.ServerCertificateValidationCallback – ab .NET 9 wird dies auf die Einstellungen von SocketsHttpHandler abgebildet.
- dotnet list package-Befehl – mit
--vulnerablelassen sich bekannte Schwachstellen prüfen. - Get-AuthenticodeSignature
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
Wie man in einer Windows-App konkret nur die „Vorgänge, die Administratorrechte brauchen“ isoliert
Eine konkrete Anleitung, wie Sie die UI einer Windows-App bei asInvoker belassen und nur die Vorgänge, die Administratorrechte brauchen, ...
Geheimnisse in Windows-Apps speichern – Klartextkonfiguration mit DPAPI vermeiden
Damit Windows-Apps Verbindungsdaten und API-Tokens nicht im Klartext in Konfigurationsdateien speichern, ordnet dieser Artikel das Konzep...
Entscheidungstabelle: Nach einer unerwarteten Ausnahme beenden oder fortsetzen?
Wenn eine unerwartete Ausnahme auftritt, ordnet dieser Artikel ein, ob die Anwendung beendet oder fortgesetzt werden sollte – aus der Per...
Die Tiefen von Windows I/O (Teil 6, Finale) ── Filtertreiber und Minifilter: Warum Procmon und Virenscanner sich in die I/O einklinken können
Der Abschluss einer bebilderten Artikelserie über Windows-Filtertreiber und Minifilter. Er behandelt den Filter-Manager und Altitudes, Pr...
Code-Design für Geschäftssysteme — Produkt- und Kundencodes festlegen, und Prüfziffern
Ein praxisnaher Leitfaden zur Festlegung des Codesystems eines Geschäftssystems, einschließlich Produkt- und Kundencodes. Behandelt eine ...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
Leistungen zu diesem Thema
Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.
Windows-App-Entwicklung
Da es dabei um die Überarbeitung einer gesamten Windows-Anwendung geht – von der Rechtegestaltung über das Distributionsverfahren und den Update-Mechanismus bis zum Logging-Design –, passt das Thema gut zur Windows-Anwendungsentwicklung.
Technische Beratung und Design-Review
Wenn Sie mit einer Sicherheitsüberprüfung einer bestehenden Anwendung, der Klärung von Rechtegrenzen oder der Neugestaltung der Updater-Strategie beginnen möchten, lässt sich das im Rahmen einer technischen Beratung und eines Design-Reviews strukturieren.
Häufige Fragen
Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.
- Was sollte bei der Sicherheit einer Windows-Anwendung als Erstes geprüft werden?
- Die vier Punkte sind: keine unnötigen Administratorrechte anfordern, den Code signieren, Geheimnisse nicht im Klartext halten und die Zertifikatsprüfung nicht deaktivieren. Bei der Mindestsicherheit geht es weniger darum, besondere Funktionen hinzuzufügen, als vielmehr darum, keine gefährlichen Standardeinstellungen und keine nachlässige Implementierung zu belassen. Das dauerhafte Überspringen der Zertifikatsprüfung, Klartext-Verbindungszeichenfolgen, ein DLL-Laden, das dem aktuellen Verzeichnis überlassen bleibt, und SQL-Ausführung per Zeichenkettenverkettung sind Punkte, die selbst auf dem Mindestniveau vermieden werden sollten.
- Darf eine Windows-Anwendung insgesamt mit Administratorrechten laufen?
- Das sollte vermieden werden. Läuft die gesamte Anwendung mit Administratorrechten, werden Fehler, DLL-Austausch, fehlerhaft gelesene Konfigurationsdateien und unzureichend geprüfte externe Eingaben direkt mit diesen starken Rechten ausgeführt. Die Grundregel lautet: Gewöhnliche UI-Anwendungen laufen standardmäßig als asInvoker, nur die Vorgänge, die Administratorrechte benötigen, werden in einen separaten Prozess oder Dienst ausgelagert, und die Rechteerhöhung erfolgt nur für den jeweils nötigen Moment.
- Wo sollten API-Schlüssel und Verbindungszeichenfolgen gespeichert werden?
- Zunächst gilt es, den Zustand zu verlassen, sie im Klartext im Quellcode oder in Konfigurationsdateien wie appsettings.json abzulegen. Für ruhende Geheimnisse wird je nach Verwendungszweck zwischen DPAPI / ProtectedData und dem Credential Locker gewählt. Werden außerdem Token, Passwörter, Verbindungszeichenfolgen oder personenbezogene Daten unverändert im Log belassen, wird das Log selbst zum Hauptdarsteller des Vorfalls, weshalb eine Maskierung nötig ist.
- Ist eine Codesignatur auch bei intern verteilten Anwendungen nötig?
- Am besten als Voraussetzung betrachten. Bei Windows-Anwendungen wird das Distributionsgut selbst (EXE / DLL / MSI / MSIX / automatisches Update-Modul) zur Angriffsfläche. Codesignatur plus Zeitstempel ermöglichen Manipulationserkennung, Vertrauenswürdigkeit gegenüber den Nutzern und eine leichtere betriebliche Nachvollziehbarkeit. Auch der Update-Mechanismus sollte die Update-Quelle festlegen und Manipulationen über HTTPS und Signaturprüfung erkennen können.
Autorenprofil
Profilseite des Artikelautors.
Go Komura
Geschäftsführer von KomuraSoft LLC
Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.