A Minimum Security Checklist for Windows App Development

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

Download the Excel version of the checklist

Talk about Windows application security tends to balloon quickly. Zero trust, EDR, SBOM, certificate operations, vulnerability management. All important — but in practice there are several basics you don’t want to miss first.

Especially for apps like the following, plugging gaps in the basics pays off more than “advanced defenses”:

  • WPF / WinForms / WinUI desktop apps
  • C++ / C# Win32 apps
  • Device integration, file integration, DB connections, internally distributed tools
  • Business apps with an auto-update mechanism
  • Setups that include Windows services or helper EXEs

In Windows app development, it is more realistic to first leave no obviously dangerous holes than to try to perfect everything at once. Here we organize the minimum points you don’t want to miss, in checklist-friendly form, in the order of design, implementation, distribution, and operations.

1. The Conclusion First

  • The first things you don’t want to miss are: don’t request administrator privileges unnecessarily, sign your binaries, don’t keep secrets in plaintext, and don’t disable certificate validation.
  • For a Windows app, the distribution artifacts themselves are an attack surface. It is safer to look at everything: EXE / DLL / MSI / MSIX / auto-update modules.
  • ServerCertificateValidationCallback => true, plaintext connection strings, careless loads like LoadLibrary("foo.dll"), and SQL built by string concatenation are items to avoid even at the minimum bar.
  • If only part of your processing needs administrator rights, it is safer to split just that part into a separate EXE or service rather than elevating the whole app.
  • Apps distributed on Windows should assume signing + timestamping. Beyond user trust, it also makes tamper detection and operational explanations easier.
  • For secrets at rest, choose between DPAPI / ProtectedData and the Credential Locker depending on the use case. At a minimum, you want to escape the state of plaintext secrets in appsettings.json.
  • More logging is not automatically better. Persist tokens, passwords, connection strings, personal information, or full request bodies as is, and the log itself becomes the protagonist of the incident.

Minimum security is less about adding special features and more about not shipping dangerous defaults and sloppy implementations.

2. Scope of This Article, and What “Minimum” Means

2.1. What we cover

The Windows apps this article has in mind are these.

  • WPF / WinForms / WinUI desktop apps
  • C++ / C# Win32 apps
  • Internally distributed tools, device-integration tools, monitoring tools
  • Setups that include helper EXEs, Windows services, and updaters
  • Business software distributed as EXE / MSI / MSIX

“Minimum” here does not mean a final state that passes an audit — it means items that, if missing, will simply cause incidents.

2.2. What we don’t cover

Some things sit outside the center of this article.

  • Organization-wide zero-trust design
  • End-to-end operation of EDR / SIEM / DLP / MDM
  • Detailed hardening of kernel drivers
  • Designing cryptographic schemes from scratch
  • Advanced threat analysis or forensic procedures

In other words, we are not covering “giant organization-wide security programs,” but the baseline a Windows app developer can and should secure on their own before release.

3. The Checklist to Look at First

Before the detailed discussion, here is a table that gives you the whole landscape. This alone is enough to spot where to start reviewing.

3.1. The big picture

Item to check Minimum action Typical anti-pattern
Execution privileges Default to asInvoker; isolate only the operations that need elevation Marking the whole app requireAdministrator
Trustworthiness of artifacts Code-sign EXE / DLL / MSI / MSIX, with timestamps Shipping unsigned
Updates Pin the update source; detect tampering via HTTPS and signature checks Downloading over HTTP and overwriting in place
Secrets Keep secrets out of source code and plaintext config; use DPAPI / Credential Locker etc. API keys and connection strings in plaintext config files
Communication Use HTTPS; never disable certificate validation Permanently skipping certificate validation with return true
External input Validate everything: SQL, files, IPC, URIs, CSV, JSON Waving it through “because it’s an internal tool”
DLL loading Use absolute paths, SetDefaultDllDirectories, and a safe search order Leaving LoadLibrary("foo.dll") to the current directory
Logging Mask tokens, passwords, PII; separate user-facing errors from internal logs Displaying or persisting exception details and connection strings as is
Dependencies Continuously update SDKs, NuGet, VC++ runtimes, OSS dependencies Freezing versions for years and ignoring vulnerability reports

3.2. Default privileges to asInvoker

This is the first thing to review in a Windows app. Run the whole app with administrator rights, and bugs, DLL substitution, misread config files, and unvalidated external input all execute with those strong privileges.

The basic policy is this.

  • Ordinary UI apps run as asInvoker
  • Only the operations that need administrator rights get split into a separate process or service
  • Elevate only for the moments that need it
  • Validate the input passed to helper EXEs and services too

If your desktop app normally only views and edits, and only installation or firewall configuration changes need administrator rights, it is safer to push just the elevated parts into a broker rather than making the whole app requireAdministrator.

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

“It’s easier if it just runs as admin” almost always comes back to bite. Run with least privilege and carve out only the operations that truly need more, and the blast radius of any accident shrinks considerably.

3.3. Sign your binaries and installers

On Windows, the trustworthiness of your distribution artifacts carries real weight. What users touch is not your source code — it is the EXE, DLL, MSI, MSIX, and updater. Leave these unsigned and your operational story, tamper detection, and distribution-time confidence all weaken.

At minimum, look at these.

  • Sign EXE / DLL / MSI / MSIX
  • Sign not just the installer but the helper binaries used for updates
  • Add timestamps
  • Include certificate expiry and renewal procedures in the release process

Signatures without timestamps in particular cause trouble during validation after the certificate expires. Rather than “it’s signed, so we’re done,” build signing + timestamping into the release procedure for stability.

If you use MSIX, package signing is a given. Even with MSI / EXE distribution, at least the installer itself and the main executable binaries should be signed.

3.4. Pin the update channel and add tamper detection

For a modern Windows app, the update channel sees far more use over its lifetime than the initial install. Get this wrong, and however carefully you built the app body, the updater becomes the weakest point.

The minimum five things to think through around updates:

  • Fetch update files over HTTPS, always
  • Verify the signature or hash of downloaded update artifacts
  • Make sure the update source URL cannot be swapped arbitrarily via code or configuration
  • Sign the update module itself
  • Decide rollback and failure-recovery procedures

If you can adopt MSIX + App Installer, you can push much of the update machinery toward the OS. If you run your own updater instead, you must verify both transport security and artifact authenticity. HTTPS protects the channel, but it does not guarantee “this file is genuinely something we published.”

3.5. Keep secrets out of source code and plaintext config

This is where real-world incidents truly happen. “It’s an internal tool,” “we’re just handing out an exe” — and connection strings, API keys, shared-folder credentials, and fixed tokens end up in source code or config files.

At minimum, avoid these arrangements.

  • API keys hard-coded in source
  • Plaintext passwords in appsettings.json or app.config
  • Connection strings checked into the repository
  • Designs that keep the decryption key and the ciphertext in the same place
  • Fixed credentials shared by everyone rather than per user

The realistic options for a Windows app come down to roughly these four.

  • You want to store Windows credentials For packaged desktop apps / WinUI, consider the Credential Locker
  • You want secrets encrypted at rest locally For Win32 / .NET, use DPAPI / ProtectedData
  • The target supports Windows authentication or integrated auth If possible, don’t make the app hold a password at all
  • Secrets can be managed on the cloud or server side Prefer designs that don’t embed long-lived secrets in the client

In C#, even just using DPAPI like this is already far better than plaintext storage.

using System.Security.Cryptography;
using System.Text;

byte[] plaintext = Encoding.UTF8.GetBytes(secretText);
byte[] ciphertext = ProtectedData.Protect(
    plaintext,
    optionalEntropy: null,
    scope: DataProtectionScope.CurrentUser);

The important thing here is not “it’s encrypted, so it’s safe” but deciding in the design who can decrypt it. CurrentUser versus LocalMachine changes the meaning considerably.

For SQL Server connections, in on-premises environments Windows authentication can often be the first choice. If you absolutely must include credentials in the connection string, at least keep Persist Security Info=False and don’t leave them sitting in plaintext config files.

3.6. HTTPS by default — and never kill certificate validation

A bypass added “just for development” ships to production untouched. That is the pattern behind most communication-related incidents.

The code and settings that most often linger in shipped builds:

  • ServicePointManager.ServerCertificateValidationCallback += ... => true
  • HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
  • Shipping with certificate revocation checks disabled
  • Leaving code that assumes development self-signed certificates in production

The minimum policy is simple.

  • Production traffic uses HTTPS
  • Never skip certificate validation unconditionally
  • If you genuinely need a validation exception, limit it to specific hosts and certificates
  • Reliably strip development bypass code via build conditions or configuration
  • In .NET, keep revocation checking in mind too

The bad example usually looks like this.

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

It looks convenient, but it behaves close to “let this HTTPS connection through no matter who it connects to.” Strip certificate validation, and even with HTTPS the substance is largely hollowed out.

3.7. Treat all external input as untrusted

Windows apps are not web apps, so input validation easily gets lax. But in reality, the entrances for external input are more numerous than you’d think.

  • File paths
  • CSV / Excel / JSON / XML
  • Command-line arguments
  • Named pipes / sockets / COM / RPC / gRPC
  • Strings passed to the DB
  • Registry values
  • The clipboard
  • URLs / deep links
  • Data returned from external devices or SDKs

The three you absolutely don’t want to miss:

  1. Always parameterize SQL Never build SQL by string concatenation.
  2. Normalize file paths before using them Never use a user-supplied path directly for delete, overwrite, or extraction.
  3. Apply size limits and format checks when reading external files “It opened, so it’s safe” is not a thing.

For SQL, this is what you want to avoid:

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

At minimum, move it to this:

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;

“It’s an internal tool, so the input is trustworthy” is a genuinely dangerous premise. In reality, corrupted CSVs, unexpected file names, stale DB data, operator typos, and half-baked JSON written by other tools walk in all the time.

3.8. Never leave DLL load locations ambiguous

This is a distinctly Windows pitfall. Load a DLL by name alone, like LoadLibrary("foo.dll"), and depending on the search order you may pick up a DLL from an unintended location.

The actions are well established.

  • Specify the DLL’s absolute path where possible
  • Set SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS) early
  • Add explicit search locations with AddDllDirectory
  • Avoid designs that pass SearchPath results straight into LoadLibrary
  • Don’t rely solely on safe DLL search mode

For native code, for example, putting this in early during process initialization is a strong pattern.

SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_DEFAULT_DIRS);

Then register only the additional directories you need with AddDllDirectory.

Because “it normally works,” this area gets neglected — and then the working directory changes at a customer site, or another product’s DLL appears in PATH, and things break silently. Beyond security, this pays off considerably as failure prevention.

3.9. Keep secrets out of logs and exceptions

Adding logs for incident investigation is important. But logs also make excellent graveyards for secrets.

The minimum review items around logging:

  • Don’t log passwords, Bearer tokens, or API keys
  • Don’t log entire connection strings
  • Mask personal information and business data payloads
  • Separate exception details between user-facing UI and internal logs
  • Don’t enable debug-grade PII logging in production
  • Review the permissions on dump / trace storage locations

Recent .NET makes redaction-first design much easier. At the very least, stop “stringify everything and log it as is.”

A few classic failures:

  • Persisting entire HTTP request / response bodies
  • Dumping the token or full headers on authentication failure
  • Showing raw exception messages in a MessageBox
  • Bundling every sensitive log into the maintenance ZIP

Separate error presentation like this, for example:

  • User-facing: “Failed to connect to the server. Please check your network settings and the URL.”
  • Internal log: target host, TLS error type, correlation ID, stack trace, retry count

This separation alone substantially improves the balance between leak prevention and diagnosability.

3.10. Don’t neglect dependencies and tooling

The last item is unglamorous but high-impact. Build the app body carefully, but ship it on top of an old runtime or dependencies with known vulnerabilities, and the floor gives way.

The list of things to watch is actually short.

  • Keep the .NET SDK / runtime on supported versions
  • Periodically review NuGet / OSS dependency updates
  • For C++, version-manage runtime redistributables and external DLLs
  • Add vulnerability-report checks to the pre-release checklist
  • Maintain smoke tests so dependency updates don’t break you silently

“We’ll batch it up later” is the most dangerous stance here. Let it sit for six months or a year, and the update delta grows so large that the security work itself becomes a heavy project.

4. Pre-Release Checklist

Here it is in a form you can use directly as a template for reviews and ship/no-ship decisions. For easy verification, the minimum pre-release items are arranged by category.

4.1. Privileges and execution model

Check item Done Notes
Normal startup runs as asInvoker  
Operations requiring admin rights are isolated into a separate EXE / service  
If a service is used, its account is no stronger than necessary  
Responsibilities under %ProgramFiles% and under user data are separated  

4.2. Distribution and signing

Check item Done Notes
EXE / DLL / MSI / MSIX / updater are signed  
Signatures carry timestamps  
Certificate expiry and renewal are part of the release flow  
Hash verification / tamper detection for artifacts is defined  

4.3. Updates

Check item Done Notes
Updates are fetched over HTTPS  
Signature or hash is verified after download  
The design makes it hard to swap the update source URL arbitrarily  
A rollback or retry policy exists for failed updates  

4.4. Secrets

Check item Done Notes
No passwords, API keys, or connection strings hard-coded in source  
No secrets in plaintext config files  
Secrets that must be stored locally are protected with DPAPI / Credential Locker etc.  
Windows authentication or user credentials are used where possible  

4.5. Communication

Check item Done Notes
Production traffic uses HTTPS  
No DangerousAcceptAnyServerCertificateValidator or => true left in shipped builds  
Revocation checking and hostname validation are accounted for  
No code or settings assuming development certificates mixed into production  

4.6. Input and data access

Check item Done Notes
SQL is parameterized  
Command-line, file, IPC, and URI inputs have size limits and format checks  
Path operations are normalized and root escape is prevented  
Raw exception messages are not shown directly on screen  

4.7. DLLs and the execution environment

Check item Done Notes
DLL load locations are explicit  
Search order is controlled via SetDefaultDllDirectories / AddDllDirectory etc.  
No DLL loading left to the current directory or PATH  
The full set of files needed for dynamic loading at deployment sites is understood  

4.8. Logging and operations

Check item Done Notes
No tokens, passwords, or PII in logs  
Internal logs and user-facing messages are separated  
Permissions on dump / trace / log storage locations have been reviewed  
SDK and dependency update status is being checked  

5. Common Anti-Patterns

What we most often see in practice are assumptions like these.

5.1. “It’s an internal tool, so it’s fine”

Internal tools still face corrupted files, operator mistakes, personal devices, shared folders, stale DLLs, and sloppy permission settings. Not being exposed to the internet does not erase the attack surface.

5.2. “It’s HTTPS, so it’s secure”

HTTPS matters, but disabling certificate validation hollows out most of its meaning. And for update distribution, you need not just HTTPS but verification of artifact authenticity.

5.3. “It’s encrypted, so it’s safe”

Without sorting out where the decryption key lives, who can decrypt, and the user and machine boundaries, encryption alone is not enough. In particular, using a LocalMachine-protected value as if it were “a per-user secret” leads to confusion later.

5.4. “More logs means easier investigation”

If the logs are voluminous but tokens and personal information pour through them, the logs themselves become the incident. If you want diagnosability, first decide what to keep and what to redact.

5.5. “Just run it as admin and the problem goes away”

Easy at first — and later it hurts in UAC, distribution, support, privilege boundaries, DLL loading, and file storage locations, roughly in that order. Least privilege is more stable over the long run.

6. Rough Priorities

If doing everything at once is too heavy, the priorities run roughly like this.

  1. Review administrator privileges First, stop habitually using requireAdministrator.
  2. Signing and timestamps Put the trustworthiness of your artifacts in order.
  3. Evacuate the secrets Get secrets out of source code and plaintext config.
  4. Fix HTTPS + certificate validation Remove the => true family from shipped builds.
  5. Review SQL / file / IPC input Reduce string concatenation and unvalidated input.
  6. Pin down DLL loading Stop name-only loads and PATH dependence.
  7. Mask the logs Make sure logs don’t become a secondary disaster during an incident.
  8. Make dependency updates routine Build the check into every release.

In this order, you can proceed in the spirit of “plug the obviously dangerous holes first.”

7. Summary

Before introducing special products or massive frameworks, Windows app security changes considerably just by putting these seven things in order: privileges, signing, secrets, communication, input, DLLs, and logging.

The minimum bar, one line each:

  • Don’t run the whole app with administrator privileges
  • Sign your artifacts and updates, with timestamps
  • Keep secrets out of source code and plaintext config
  • Use HTTPS — and don’t kill certificate validation
  • Don’t trust external input: SQL, files, IPC, and the rest
  • Never leave DLL load locations ambiguous
  • Keep secrets out of logs
  • Don’t neglect your dependencies

Security is a broad subject, but you don’t have to do all of it from day one. The one minimum worth establishing very early, though, is this: never ship dangerous defaults as they are.

8. References

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

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

This article connects naturally to the following service pages.

Windows App Development

Reviewing a Windows application end to end - privilege design, distribution method, update mechanism, and logging design - is a natural fit for our Windows application development service.

Author Profile

Profile page for the article author.

Go Komura

Representative of KomuraSoft LLC

Focused on Windows software development, technical consulting, and investigations into failures that are difficult to reproduce.

Back to the Blog