A Minimum Security Checklist for Windows App Development
· Go Komura · 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 likeLoadLibrary("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.jsonorapp.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 += ... => trueHttpClientHandler.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:
- Always parameterize SQL Never build SQL by string concatenation.
- Normalize file paths before using them Never use a user-supplied path directly for delete, overwrite, or extraction.
- 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
SearchPathresults straight intoLoadLibrary - 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.
- Review administrator privileges
First, stop habitually using
requireAdministrator. - Signing and timestamps Put the trustworthiness of your artifacts in order.
- Evacuate the secrets Get secrets out of source code and plaintext config.
- Fix HTTPS + certificate validation
Remove the
=> truefamily from shipped builds. - Review SQL / file / IPC input Reduce string concatenation and unvalidated input.
- Pin down DLL loading Stop name-only loads and PATH dependence.
- Mask the logs Make sure logs don’t become a secondary disaster during an incident.
- 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
- 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
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
How to Concretely Isolate "Only the Operations That Need Administrator Privileges" in a Windows App
A concrete walkthrough of keeping a Windows app UI at asInvoker while isolating only the administrator-privileged operations into a helpe...
Storing Secrets in Windows Apps - Avoiding Plaintext Configuration with DPAPI
To avoid storing connection credentials and API tokens in plaintext configuration files in Windows apps, we walk through DPAPI / Protecte...
Why Windows Became What It Is Today: The Evolution of Windows Through a Developer's Eyes
A look at the changes from Windows 95 to Windows 11 — not as a visual timeline, but from a Windows application developer's perspective: c...
A Decision Table for Whether to Exit or Continue After an Unexpected Exception
When an unexpected exception occurs, should the app exit or keep running? We organize the decision from the perspectives of state corrupt...
Handling Windows Impersonation Tokens Correctly — Borrowing Privileges per Thread and Reverting Safely
A practical guide to Windows impersonation tokens — access tokens, primary tokens, thread tokens, impersonation levels, RevertToSelf, and...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
Where This Topic Connects
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.
Technical Consulting & Design Review
If you want to start with a security review of an existing app, sorting out privilege boundaries, or redesigning your updater policy, we can structure that as technical consulting and design review.
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.
Public links