A Minimum Security Checklist for Windows Application Development

· · Windows Development, Security, Architecture, C# / .NET, Win32

Bottom line

The first things you do not want to skip in Windows application security are: don’t request administrator privileges you don’t need, sign your binaries, don’t keep secrets in plain text, and don’t disable certificate validation.

Closing the basic gaps is more practical than reaching for advanced defenses first.

The checklist at a glance

Item Minimum to do Typical anti-pattern
Execution privilege Default to asInvoker; isolate only the parts that need elevation Marking the whole app as requireAdministrator
Trust of distributed binaries Code-sign EXE/DLL/MSI/MSIX with a timestamp Shipping unsigned
Updates Use HTTPS and verify signatures to detect tampering Downloading over HTTP and overwriting as-is
Secrets Protect with DPAPI, Credential Locker, etc. API keys or connection strings stored in plain text
Communication Use HTTPS and don’t disable certificate validation Always skipping with return true
External input Validate input from SQL, files, IPC, etc. “It’s just an internal tool” so no checks
DLL loading Use absolute paths and a safe search order LoadLibrary("foo.dll") relying on the current directory
Logs Mask tokens, passwords, personal data Dumping exception details or connection strings as-is

1. Privileges: default to asInvoker

If the entire app runs as administrator, every bug, swapped DLL, or misread config file gets that strong privilege for free.

<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
  <security>
    <requestedPrivileges>
      <requestedExecutionLevel level="asInvoker" uiAccess="false" />
    </requestedPrivileges>
  </security>
</trustInfo>
  • Use asInvoker for normal UI applications
  • Move work that truly requires administrator privileges into a separate process or service
  • Elevate only at the moment it is actually needed

2. Sign what you ship

On Windows, the binaries you distribute are themselves an attack surface. At minimum:

  • Sign EXE/DLL/MSI/MSIX
  • Sign helper binaries used for updates, not just the installer
  • Add a timestamp so signatures still verify after the certificate expires
  • Track certificate expiration and the renewal procedure as part of your release process

3. Secure the update path

  • Fetch update files over HTTPS
  • Verify the signature or hash of the downloaded update
  • Don’t let the update URL be swapped freely from code or config
  • Sign the updater itself
  • Decide on a rollback or recovery procedure for failed updates

HTTPS protects the transport, but on its own it does not prove that the file is really something you published.

4. Don’t keep secrets in plain text

Things to avoid at all costs:

  • API keys hardcoded in source
  • Plain-text passwords in appsettings.json
  • Connection strings checked into the repository
  • A design where the decryption key sits next to the ciphertext

Practical options:

  • If Windows or integrated authentication is available, don’t make the app hold a password at all
  • For local storage, use DPAPI / ProtectedData (typically CurrentUser)
  • If secrets can be managed on the cloud side, don’t embed them in the client
byte[] plaintext = Encoding.UTF8.GetBytes(secretText);
byte[] ciphertext = ProtectedData.Protect(
    plaintext, null, DataProtectionScope.CurrentUser);

5. Communication: don’t kill certificate validation

Particularly dangerous code:

// Never leave this in production
ServicePointManager.ServerCertificateValidationCallback += (_, _, _, _) => true;
  • Use HTTPS for production traffic
  • Don’t permanently bypass certificate validation
  • Make sure development workarounds are stripped out by build conditions or configuration
  • On .NET, also keep revocation checks in mind

6. Validate external input

A Windows app actually has a lot of input entry points:

  • File paths; CSV, Excel, JSON, XML; command-line arguments
  • Named pipes, sockets, COM, RPC; database queries; registry values
  • Clipboard, URLs and deep links, data from external devices

The bare minimum:

  1. Always parameterize SQL (no string concatenation)
  2. Normalize file paths before using them
  3. Apply size limits and format checks when reading external files
// Good
cmd.CommandText = "SELECT * FROM Users WHERE Name = @name";
cmd.Parameters.Add("@name", SqlDbType.NVarChar, 256).Value = userName;

7. Be explicit about where DLLs are loaded from

// Early in process startup
SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);
  • Where possible, load DLLs by absolute path
  • Don’t rely on the current directory or PATH
  • Register only the additional directories you actually need with AddDllDirectory

8. Don’t leak secrets through logs and exceptions

  • Don’t log passwords, bearer tokens, or API keys
  • Don’t log full connection strings
  • Separate user-facing exception messages from internal logs
  • Keep error dialogs to something like “failed to connect to the server” and put the details in internal logs

Pre-release checklist (excerpt)

Privileges and execution

  • The app runs under asInvoker for normal use
  • Work that requires administrator privileges is split into a separate EXE or service

Distribution and signing

  • EXE/DLL/MSI/MSIX/updater are all signed
  • Signatures include a timestamp

Secrets

  • No passwords or API keys are hardcoded in source
  • No secrets sit in plain-text config files

Communication

  • Production traffic uses HTTPS
  • No => true callbacks shipped with the product

Input

  • SQL is parameterized
  • Command-line, file, and IPC input have size limits and format checks

DLL loading

  • DLL load locations are made explicit
  • Nothing relies on the current directory

Logs

  • Tokens, passwords, and personal data are not written to logs
  • Internal logs and user-facing messages are separated

Common anti-patterns

  1. “It’s just an internal tool, so it’s fine” — corrupted files, mistakes, shared folders, and stale DLLs all happen in normal operations
  2. “It’s HTTPS, so it’s safe” — disabling certificate validation drains most of that value
  3. “It’s encrypted, so it’s safe” — without thinking through where the key lives and the privilege boundaries, encryption alone is not enough
  4. “More logging means we can investigate anything” — if logs leak secrets, the logs themselves become the incident
  5. “Just run it as admin and it works” — easy in the short term, painful at the privilege boundary in the long run

Priority order

  1. Revisit administrator privileges (stop using requireAdministrator by default)
  2. Code signing and timestamps
  3. Move secrets out of source code and plain-text config
  4. HTTPS plus correct certificate validation
  5. Review SQL, file, and IPC input handling
  6. Lock down DLL loading
  7. Mask sensitive data in logs
  8. Make dependency updates a routine

Wrap-up

Before introducing a special product or a large framework, just tightening these seven areas - privileges, signing, secrets, communication, input, DLL loading, and logs - already changes the security picture significantly. Start by not shipping unsafe defaults as-is.

Related Articles

Recent articles sharing the same tags. Deepen your understanding with closely related topics.

Related Topics

These topic pages place the article in a broader service and decision context.

Where This Topic Connects

This article connects naturally to the following service pages.

Back to the Blog