Security & App Isolation
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
A desktop application runs with the full authority of the user who started it. By default it can read every file that user can read, call every service the user can reach, and read any secret the user’s session exposes. That is the opposite of a phone app, and it is why security on Windows is mostly a matter of narrowing what the app can do, protecting what it stores, trusting only what it can verify, and proving where it came from. This page covers each of those four jobs for Bookshelf Desktop, the running scenario of this section, and ends by mapping the security checklist of a book on CLI applications onto a desktop app.
The page was written against these versions, current in October 2026:
-
.NET 10 LTS, latest patch 10.0.13, with C# 14. .NET 11 is at release candidate 2 (general availability is expected 10 to 13 November 2026); nothing on this page depends on a .NET 11 change. .NET 8 and .NET 9 both end support on 10 November 2026, which matters for the "supported frameworks" item of the checklist.
-
Visual Studio 2026 18.11.1 and the Windows App SDK 2.5.1 for WinUI 3 projects.
-
CommunityToolkit.Mvvm 8.4.2 for the view models. Security concerns in this page live in services behind interfaces, so the view models never touch them directly.
-
Windows 11 and Windows 10. Windows 10 reached the end of support on 14 October 2025, which is itself a security fact: an unsupported operating system gets no security updates.
Packaging itself (building, signing and distributing an MSIX) belongs to the publishing pages of this section. This
page covers only the security consequences of those choices. The shared interfaces used below (IBookRepository,
IDialogService) are the ones introduced in MVVM Toolkit and
Navigation, Dialogs & Services in MVVM.
The Threat Model Before the Features
Security features answer specific threats, so name the threats first. A lightweight STRIDE pass over Bookshelf Desktop (the method is the one the book discussed later on this page recommends) gives a short list:
| STRIDE category | Threat for Bookshelf Desktop | Control on this page |
|---|---|---|
Spoofing |
A fake update feed or a tampered installer pretends to be Bookshelf. |
Code signing, SmartScreen reputation, HTTPS-only update feeds. |
Tampering |
Malware edits the installed files or the local SQLite database. |
MSIX read-only install location, signed binaries, file ACLs on the data folder. |
Repudiation |
A user denies deleting a book in a shared library. |
Application logging without secrets; server-side audit (outside the app). |
Information disclosure |
The sync token is read from a settings file or a log. |
DPAPI or Credential Locker storage, log redaction. |
Denial of service |
A huge imported file or hostile XAML freezes the UI thread. |
Input limits, off-thread parsing, no run-time XAML from untrusted sources. |
Elevation of privilege |
A flaw in book-cover decoding runs code with admin rights. |
Run as |
The point of the exercise is the right-hand column: every control below exists because a row above needs it. Treating the list as a living document (re-run it whenever a feature adds a file format, a network call or a plug-in point) is more valuable than any single setting.
The Three Trust Tiers
A Windows desktop process lives in exactly one of three tiers, and the tier decides what the rest of the page can do for you.
-
Unpackaged (an
.exefrom an MSI, ClickOnce, a zip ordotnet publish). The process runs at medium integrity with the user’s token. The OS offers no isolation beyond the user boundary, so everything is the app’s own job. -
Packaged, full trust (
uap10:TrustLevel="mediumIL"). This is the default for WinUI 3 desktop apps packaged as MSIX, and the packaging used for WPF and WinForms through a packaging project. The process is not in an AppContainer. ItsAppDataandHKCUwrites are virtualized (thepackagedClassicAppruntime behaviour); an app packaged with external location (win32App) also has identity but is not virtualized. Packaging buys integrity of the install and a clean uninstall, not a sandbox. -
Packaged, AppContainer (
uap10:TrustLevel="appContainer"). The process and its children run in a lightweight app container and can reach only what the manifest grants. This is the only tier that is an actual sandbox.
The Windows App SDK documentation states it plainly: WinUI 3 desktop apps packaged as MSIX typically run at medium integrity level (full trust) in a less restrictive sandbox, not in an AppContainer, unless you explicitly configure them as AppContainer apps. Do not describe "an MSIX app" as sandboxed without saying which tier you mean.
What MSIX Packaging Gives a Full-Trust App
Even in full trust, package identity changes how the OS treats the install, and some of it is a security benefit.
| Behaviour | Security meaning |
|---|---|
Per-user install, files marked read-only under |
Another process or the user cannot silently modify the binaries; the OS refuses to launch a package whose files were tampered with. |
Package is read-only; writes inside it are not allowed |
An exploit cannot persist by patching the app’s own DLLs. |
New files under |
State is separated from other apps and removed cleanly at uninstall. |
Writes under |
The registry footprint is tracked and removable. |
Writes outside the package and outside those redirected folders are allowed if the user has permission |
Virtualization is for compatibility and cleanup, not a security boundary: a full-trust app can still write to other user-writable locations. |
The last row is the one people miss. File and registry virtualization in a full-trust package exists so that old apps keep working and uninstall leaves nothing behind. It does not stop the app (or code injected into it) from touching other files the user can touch. For that you need the AppContainer tier.
Capabilities and runFullTrust
A capability is a manifest declaration that grants an AppContainer app access to a protected resource. They are
declared in Package.appxmanifest under Capabilities and fall into general-use, device, restricted and custom
categories. The relevant facts for a desktop developer are:
-
A full-trust (medium integrity) packaged app needs the
runFullTrustrestricted capability, declared with therescapnamespace prefix. It is what allows a package that contains full-trust apps to be installed. -
internetClientandenterpriseAuthenticationgrant operations the user can already perform, so they matter only to AppContainer apps; a medium integrity app runs as the user and does not need them. -
internetClientServerincludes the access ofinternetClientand adds the ability to act as a server. -
Restricted capabilities require an explanation during Microsoft Store certification (entered on the Partner Center submission options page), but Store approval is not required to sideload a package that declares them.
-
A medium integrity app should still declare capabilities for privacy-sensitive resources (location, camera, microphone and similar), because the user can change that access in Windows privacy settings and the app must handle it being denied.
Full trust and AppContainer manifests
The two tiers differ by a few lines of the manifest. This is the full-trust shape the WinUI 3 template generates, followed by the AppContainer variant for Windows 10 version 2004 and later.
<!-- Full trust: the default for a packaged WinUI 3 app -->
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="rescap">
<Applications>
<Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="$targetentrypoint$">
<!-- visual elements omitted -->
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>
<!-- AppContainer: no runFullTrust, an explicit trust level, and only the capabilities the app uses -->
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10"
IgnorableNamespaces="uap10">
<Applications>
<Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="$targetentrypoint$"
uap10:TrustLevel="appContainer"
uap10:RuntimeBehavior="packagedClassicApp">
<!-- visual elements omitted -->
</Application>
</Applications>
<Capabilities>
<Capability Name="internetClient" />
</Capabilities>
</Package>
Source: MSIX AppContainer apps. The capability list is
in App capability declarations.
The internetClient line in the second manifest is the example this page adds, because Bookshelf Desktop’s sync
needs outbound network access once it no longer runs as the user.
For a WPF or WinForms project, the same switch is made on the packaging project: expand
Dependencies > Applications, select the reference to the application and set Trust Level to Partial Trust in
the Properties window, which writes a TrustLevel of Partial on the project reference.
What AppContainer isolates
Microsoft describes the AppContainer environment as isolation along six axes. Each is a concrete reason to prefer the tier for code that parses untrusted data.
| Axis | What the AppContainer does |
|---|---|
Credential |
Credentials are tied to a user and application pairing; the app cannot use the user’s credentials to log in elsewhere or impersonate the user. |
Device |
Sensors such as camera, microphone and GPS, and "money pumps" such as mobile broadband, are blocked by default and granted per capability. |
File and registry |
The app cannot modify files it should not; read-write access is granted to specific persistent files and registry keys. |
Network |
Access to the Internet, intranet and acting as a server are separate grants. |
Process |
Kernel objects are sandboxed so the app cannot influence, or be influenced by, other processes. |
Window |
The app cannot affect the windows of other applications. |
The documentation’s own reasoning is worth keeping: most vulnerabilities start in the application, and even if one is exploited, the app cannot reach beyond what the container grants.
When AppContainer is practical for a WPF or WinForms app
The tier is not free. An app moved into an AppContainer loses implicit access to the user’s documents, to arbitrary network endpoints, to most of the registry and to other processes. Plan for these consequences before enabling it:
-
Use pickers (
FileOpenPicker, or the common file dialogs brokered by the system) and declared capabilities rather than raw paths into the user’s folders. -
Any component that calls
DeploymentManager.Initializein an AppContainer process needs thepackageManagementrestricted capability. -
Third-party libraries that assume a writable install folder, the whole registry or a loopback listener may fail.
-
Out-of-process COM servers and some interop paths need
runFullTrustand therefore a full-trust helper process.
A common middle path is a split design: a small, full-trust broker that does the privileged work, and an AppContainer UI or parser process that handles untrusted input. That is more engineering than a manifest edit, so reserve it for the parts that read hostile data (importing book files from the Internet is the Bookshelf example).
Win32 app isolation (preview)
Win32 app isolation is a newer, still-in-preview sandbox aimed at existing Win32 apps, Desktop Bridge apps and apps
packaged with external location, with "little to no change to your code". It needs Windows 11 version 24H2
(build 26100) or later and Visual Studio 17.10.2 or later. The app is still packaged as MSIX, but the Application
element uses the uap18 namespace instead of the uap10 attributes shown above:
<Application Id="App"
Executable="Bookshelf.Desktop.exe"
EntryPoint="Windows.FullTrustApplication"
uap18:EntryPoint="Isolated.App"
uap18:TrustLevel="appContainer"
uap18:RuntimeBehavior="appSilo">
TargetDeviceFamily gets MinVersion="10.0.26100.0", and runFullTrust should be removed unless another manifest
extension (such as comServer) needs it. On an operating system without the feature, the same package keeps running
as an ordinary Win32 app. Microsoft’s Application Capability Profiler helps find the capabilities an isolated app
needs. An isolated Win32 app cannot share a package with other application types. Because the feature is in preview,
treat it as an option to evaluate for Bookshelf’s import path, not as the baseline.
Running Elevated or Not: UAC and requestedExecutionLevel
User Account Control lets users work as standard users while administrators run most programs with a filtered
token. A desktop app declares what it needs in an application manifest (the Win32 side-by-side manifest embedded in
the .exe, distinct from Package.appxmanifest) through the requestedExecutionLevel element under trustInfo.
level value |
Behaviour |
|---|---|
|
Runs with the permission level of the process that started it. The user can still choose Run as administrator. |
|
Runs at the highest level the user has. For a member of the Administrators group this is the same as |
|
Always runs with administrator permissions; the system prompts for credentials if the opening process is not elevated. |
The optional uiAccess attribute lets an application bypass user-interface protection levels and drive input to
higher-privilege windows. It defaults to false and Microsoft says to set it to true only for user interface
accessibility applications, subject to security policy that can require the app to be installed in a secure
location. Bookshelf Desktop is not an assistive technology and leaves it false.
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="Bookshelf.Desktop.app"/>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
Source: Application manifests.
Add the file to the project as app.manifest and point the SDK-style project at it:
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
Source: WPF documentation for the project shape; the
ApplicationManifest property is the standard MSBuild way to embed a manifest.
Rules for elevation
-
Default to
asInvoker. An app that never elevates cannot be tricked into misusing elevation. -
Specifying a
requestedExecutionLevelnode disables file and registry virtualization for that app. Omit the node only if you deliberately depend on virtualization for legacy compatibility, which a new app should not. -
Do not make the whole app
requireAdministratorbecause one feature (installing a shared font, writing toProgram Files) needs it. Run that feature in a separate, narrowly scoped elevated helper, started on demand. -
A packaged app’s install folder is read-only, so "write next to the exe" designs fail for MSIX regardless of elevation. Store data in the per-user data folder instead.
-
Never trust that an elevated process is safe to load user-supplied plug-ins or XAML; an exploit there has administrator rights.
Storing Secrets
Bookshelf Desktop can sync a library with a remote service, so it holds a long-lived token. The wrong places are the usual ones: a settings file, an embedded constant, an environment variable written to disk, a log line. The Windows options are summarised below.
| Store | Scope | Strengths | Limits |
|---|---|---|---|
DPAPI through |
Current user, or the machine |
No key to manage; the OS derives protection from the account; optional extra entropy. |
Windows only; the protected blob is only as safe as the user’s logon session; you choose and secure the file it is saved to. |
Credential Locker ( |
Per user |
Managed by the OS; an AppContainer app sees only its own locker. |
A desktop (non-AppContainer) app can access all of the user’s lockers, including those of AppContainer apps. WinRT API, needs a Windows-targeted TFM. |
Credential Manager (Win32 |
Per logon session |
The same store users see in Control Panel. |
A native API; from .NET it needs a P/Invoke layer or a library. |
Server-side token (OAuth/OIDC) |
Server |
The device holds a short-lived token that can be revoked. |
Needs a service and a sign-in flow. |
|
Anyone with file access |
Simple. |
Not secret storage. Never use for tokens or passwords. |
DPAPI is documented as a way to encrypt data with information from the current user account or the computer so you
avoid the difficult problem of generating and storing a key yourself. ProtectedData is available on .NET and
.NET Framework (the in-memory counterpart, ProtectedMemory, is documented for .NET Framework 2.0 to 4.8.1 only, see
ProtectedMemory class), and on .NET it ships in the
System.Security.Cryptography.ProtectedData NuGet package. DataProtectionScope.CurrentUser limits decryption to
the same user account; DataProtectionScope.LocalMachine lets any account on the computer decrypt, which is almost
never what a per-user token wants.
A secret store service
The view model needs a token only to pass it to the sync client, so hide storage behind an interface. That keeps the Windows-only code replaceable and testable.
namespace Bookshelf.Desktop.Security;
public interface ISecretStore
{
ValueTask SaveAsync(string name, string secret, CancellationToken cancellationToken = default);
ValueTask<string?> LoadAsync(string name, CancellationToken cancellationToken = default);
ValueTask DeleteAsync(string name, CancellationToken cancellationToken = default);
}
using System.Security.Cryptography;
using System.Text;
namespace Bookshelf.Desktop.Security;
/// <summary>Stores secrets as DPAPI blobs, one file per secret, in a per-user folder.</summary>
public sealed class DpapiSecretStore(string folder) : ISecretStore
{
// Extra entropy ties the blob to this application: another app running as the same user
// cannot decrypt it without also knowing this value. It is not a secret key.
private static readonly byte[] Entropy = "Bookshelf.Desktop.v1"u8.ToArray();
private string PathFor(string name) => Path.Combine(folder, Uri.EscapeDataString(name) + ".bin");
public async ValueTask SaveAsync(string name, string secret, CancellationToken cancellationToken = default)
{
ArgumentException.ThrowIfNullOrWhiteSpace(name);
ArgumentException.ThrowIfNullOrEmpty(secret);
Directory.CreateDirectory(folder);
byte[] plain = Encoding.UTF8.GetBytes(secret);
try
{
byte[] blob = ProtectedData.Protect(plain, Entropy, DataProtectionScope.CurrentUser);
await File.WriteAllBytesAsync(PathFor(name), blob, cancellationToken);
}
finally
{
CryptographicOperations.ZeroMemory(plain);
}
}
public async ValueTask<string?> LoadAsync(string name, CancellationToken cancellationToken = default)
{
string path = PathFor(name);
if (!File.Exists(path))
{
return null;
}
byte[] blob = await File.ReadAllBytesAsync(path, cancellationToken);
try
{
byte[] plain = ProtectedData.Unprotect(blob, Entropy, DataProtectionScope.CurrentUser);
try
{
return Encoding.UTF8.GetString(plain);
}
finally
{
CryptographicOperations.ZeroMemory(plain);
}
}
catch (CryptographicException)
{
// Copied from another user or machine, or corrupted: treat as "no secret", ask the user to sign in again.
return null;
}
}
public ValueTask DeleteAsync(string name, CancellationToken cancellationToken = default)
{
File.Delete(PathFor(name));
return ValueTask.CompletedTask;
}
}
Register it in the Generic Host (see Dependency
Injection & the Generic Host) with the folder under Environment.SpecialFolder.LocalApplicationData, and add the
package to the project:
<ItemGroup>
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="10.0.13" />
</ItemGroup>
Source: How to use data protection and System.Security.Cryptography.ProtectedData on NuGet (10.0.13 is the current release). The documentation sample creates random entropy and stores it; the version above uses a fixed per-application value to bind the blob to the app. If you need entropy that other same-user apps cannot guess, generate it randomly and keep it in a second protected location, as the sample does.
A Credential Locker alternative
When the project targets a Windows version-specific framework (for example net10.0-windows10.0.19041.0), the
WinRT PasswordVault can be used instead. A credential is addressed by a resource name and a user name.
using Windows.Security.Credentials;
namespace Bookshelf.Desktop.Security;
public sealed class VaultSecretStore : ISecretStore
{
private const string Resource = "Bookshelf.Desktop/sync";
public ValueTask SaveAsync(string name, string secret, CancellationToken cancellationToken = default)
{
new PasswordVault().Add(new PasswordCredential(Resource, name, secret));
return ValueTask.CompletedTask;
}
public ValueTask<string?> LoadAsync(string name, CancellationToken cancellationToken = default)
{
try
{
PasswordCredential credential = new PasswordVault().Retrieve(Resource, name);
credential.RetrievePassword();
return ValueTask.FromResult<string?>(credential.Password);
}
catch (Exception ex) when (ex.HResult == unchecked((int)0x80070490))
{
// ERROR_NOT_FOUND: nothing stored under this resource and user name.
return ValueTask.FromResult<string?>(null);
}
}
public ValueTask DeleteAsync(string name, CancellationToken cancellationToken = default)
{
PasswordVault vault = new();
try
{
vault.Remove(vault.Retrieve(Resource, name));
}
catch (Exception ex) when (ex.HResult == unchecked((int)0x80070490))
{
// Already gone.
}
return ValueTask.CompletedTask;
}
}
Source: PasswordVault class.
The class documents Add, Retrieve, Remove, RetrieveAll, FindAllByResource and FindAllByUserName. The
class page does not document which exception a missing credential raises; the example treats 0x80070490
(ERROR_NOT_FOUND as an HRESULT) as "not stored yet" and lets any other error propagate.
The class documentation also says that apps not running in an AppContainer, such as regular desktop apps, can access all of the user’s lockers. A full-trust desktop app therefore gains no isolation from the locker: any other full-trust app running as the same user can enumerate it. That is a property of the tier, not a flaw to code around.
Secret handling rules
-
Treat the string returned by
LoadAsyncas short-lived: pass it to the HTTP client and drop it. Managed strings cannot be wiped, which is why the DPAPI example zeroes the byte arrays it controls. -
Never put tokens in
appsettings.json, in the project file, in command-line arguments or in aWindowsAppspackage. Anything shipped in the package is readable by the user and by malware running as them. -
Read configuration secrets for builds and tests from environment variables or the CI secret store, never from the repository.
-
Delete the stored secret on sign-out and when the account is removed.
-
Do not log secrets. See the logging item in the checklist below.
Safe Use of XamlReader
System.Windows.Markup.XamlReader loads XAML at run time and returns the root of the object tree. XAML can
instantiate arbitrary types and set their properties, so loading markup from an untrusted file, e-mail attachment or
server response is equivalent to running code chosen by whoever wrote that markup. The language-level mechanics are
in XAML Language Fundamentals; the security rules follow.
-
Do not load untrusted XAML at all if you can avoid it. Use data (JSON, a typed DTO) plus a
DataTemplateyou authored. -
For WPF on .NET, the
Load(Stream, Boolean),Load(XmlReader, Boolean)andLoad(Stream, ParserContext, Boolean)overloads takeuseRestrictiveXamlReader. Passingtruerestricts instantiation of potentially dangerous types. -
Restrictive mode is a defense-in-depth measure only. The documentation says not to treat a restrictive parse of untrusted XAML as safe or as a replacement for isolation in a low-privilege sandbox.
-
If you must accept user-authored layouts, parse them in a separate AppContainer process and send back only the data you need.
using System.IO;
using System.Windows;
using System.Windows.Markup;
namespace Bookshelf.Desktop.Security;
public static class UserTemplateLoader
{
/// <summary>Loads a user-supplied card layout. Defense in depth only: the caller must also isolate the work.</summary>
public static DataTemplate? TryLoadCardTemplate(Stream untrustedXaml)
{
try
{
object root = XamlReader.Load(untrustedXaml, useRestrictiveXamlReader: true);
return root as DataTemplate;
}
catch (XamlParseException)
{
return null;
}
catch (InvalidOperationException)
{
return null;
}
}
}
Source: XamlReader.Load method.
The class-level remarks of the restrictive overloads are the basis of the warning above. The two catch clauses are
a Bookshelf choice; the page lists ArgumentNullException as the only documented exception, so test which exception a malformed or
blocked document throws with the documents you expect.
For WinUI 3 and UWP, the equivalent loader is Microsoft.UI.Xaml.Markup.XamlReader; the restrictive overloads are
documented for WPF only, so the guidance to avoid loading untrusted markup applies even more strongly there.
Dependencies, Supply Chain and NuGet Audit
A desktop app ships its dependencies to every customer, so a vulnerable package is a vulnerability in each installed copy. NuGet checks restored packages against vulnerability data on nuget.org, which is sourced from the GitHub Advisory Database.
| MSBuild property | Default | Meaning |
|---|---|---|
|
|
Set to |
|
|
|
|
|
The minimum severity reported: |
Audit raises warnings NU1901 (low), NU1902 (moderate), NU1903 (high) and NU1904 (critical); NU1900 means the vulnerability data could not be retrieved and NU1905 means an audit source provides no vulnerability database. A sensible policy is to keep audit warnings non-blocking for everyday builds and treat them as errors in one dedicated pipeline run, so that a newly published advisory does not break an unrelated bug fix at an awkward moment.
<!-- Directory.Build.props -->
<Project>
<PropertyGroup>
<NuGetAudit>true</NuGetAudit>
<NuGetAuditMode>all</NuGetAuditMode>
<NuGetAuditLevel>moderate</NuGetAuditLevel>
<NuGetAuditCodes>NU1900;NU1901;NU1902;NU1903;NU1904;NU1905</NuGetAuditCodes>
<WarningsAsErrors Condition=" '$(AuditPipeline)' == 'true' ">$(WarningsAsErrors);$(NuGetAuditCodes)</WarningsAsErrors>
<WarningsNotAsErrors Condition=" '$(AuditPipeline)' != 'true' ">$(WarningsNotAsErrors);$(NuGetAuditCodes)</WarningsNotAsErrors>
</PropertyGroup>
</Project>
# Everyday: warnings only
dotnet restore
# Dedicated audit pipeline: fail on any audit warning
dotnet restore -p:AuditPipeline=true
# Inspect and fix by hand
dotnet list package --vulnerable --include-transitive
dotnet list package --outdated
dotnet nuget why Bookshelf.Desktop.csproj Some.Vulnerable.Package
Source: Auditing package dependencies for security vulnerabilities.
The dotnet package update --vulnerable command (NuGet 7.0, .NET 10 SDK) updates vulnerable packages to the first
version without known vulnerabilities. The page notes that --include-transitive is not the default for
dotnet list package --vulnerable, so pass it. For a vulnerable transitive package, first try updating the direct
package closest to it, then pin the fixed version, and suppress an advisory with a NuGetAuditSuppress item only as
a last resort after analysing it.
Two related habits belong here: keep the target framework on a supported release (.NET 8 and .NET 9 end support on
10 November 2026, .NET 10 LTS on 14 November 2028), and prefer a single trusted feed that upstreams from nuget.org,
using auditSources in NuGet.Config so the vulnerability lookup does not force nuget.org as a package source.
Signed Binaries and SmartScreen
Signing answers "who published this, and was it changed?" Two Windows features read that answer.
Microsoft Defender SmartScreen checks the reputation of downloaded files from two signals: publisher reputation (is the file signed by a known, trusted publisher?) and file-hash reputation (has this exact file been downloaded without signs of malicious behaviour?). A new binary can still show a warning until reputation accumulates.
| Distribution | First-download SmartScreen behaviour |
|---|---|
Microsoft Store |
No warning; the package is covered by Microsoft’s certificate. |
Valid OV or EV certificate |
Warning until reputation accumulates; the verified publisher name is shown. |
No signature or self-signed |
"Windows protected your PC": the user must choose to run anyway, and enterprise policy can prevent continuing. |
Points that change how you plan a release:
-
EV certificates no longer bypass SmartScreen. The documentation says paying a premium for EV solely to avoid warnings is no longer justified, although EV may matter for enterprise procurement.
-
Reputation builds organically through download volume; there is no manual submission for consumer endpoints, and it can take weeks and many clean installs.
-
Unsigned files rebuild reputation from zero for every version. A consistent signing identity lets reputation carry over between versions, and changing the certificate resets the publisher trust signal.
-
Do not modify a file after signing, and do not sign anything that could be flagged as unwanted software.
-
On Windows 11, Smart App Control can supersede SmartScreen application reputation and blocks unsigned executables that lack a positive reputation, for all executables and not only downloads.
-
Artifact Signing (formerly Trusted Signing) is Microsoft’s recommended signing service for distribution outside the Store; it needs no hardware token and integrates with GitHub Actions and Azure DevOps.
Whatever the certificate source, the pipeline must read the key material from secrets. A command-line example with
signtool that never contains a credential:
# SIGN_CERT_PATH points to a certificate file the CI job restored from its secret store;
# SIGN_CERT_PASSWORD comes from a CI secret. Neither value is ever written to the repository.
signtool sign /fd SHA256 /td SHA256 /tr $env:TIMESTAMP_URL `
/f $env:SIGN_CERT_PATH /p $env:SIGN_CERT_PASSWORD `
artifacts\Bookshelf.Desktop.msix
signtool verify /pa /v artifacts\Bookshelf.Desktop.msix
Source: SmartScreen reputation for Windows app developers
for the reputation rules. The signtool switches shown are the standard ones; the signing and packaging pages of
this section cover the full workflow, including the MSIX requirement that the manifest Publisher match the
certificate subject.
Update-Channel Security
An update mechanism is a remote-code-execution path by design, so it deserves its own review. The controls are the same for every channel; what differs is who enforces them.
| Channel | Who verifies | What you must still do |
|---|---|---|
Microsoft Store |
The Store signs and delivers the package. |
Keep certification-relevant declarations (capabilities) honest. |
App Installer feed |
Windows checks the package signature and the feed you publish. |
Serve the feed and packages over HTTPS from a controlled location; sign every package; protect the publishing credentials. |
WinGet |
The manifest carries the installer hash that WinGet checks. |
Publish the correct hash for the signed installer; review community-manifest pull requests. |
ClickOnce |
The application manifest is signed and the update is verified against it. |
Keep the signing key off developer laptops; plan certificate expiry. |
Custom updater |
Your code. |
Verify a signature on the downloaded payload before running it; pin the publisher; never download-then-run over plain HTTP. |
The channel rows summarise how each distribution page of this section behaves; follow them for the specifics. Rules that hold everywhere:
-
Use HTTPS only, and treat the transport as a convenience rather than the proof. The proof is the signature on the payload.
-
Verify before you execute. If you write your own updater, check the signature and the expected publisher on the downloaded file before launching it, and fail closed.
-
Protect the release pipeline as carefully as the app: restrict who can trigger a release, keep signing material in the CI secret store with role-based access, and use environment protection for the signing step.
-
Support rollback: a bad update should be revocable, and an update-check failure must never block the app.
-
Never let the update feed decide what to run beyond a package you signed; do not accept arbitrary URLs from the feed.
Security Across the Three Stacks
| Topic | WPF | WinUI 3 / Windows App SDK | Windows Forms |
|---|---|---|---|
Default tier |
Unpackaged, or full trust in MSIX |
Full trust in MSIX; unpackaged also supported |
Unpackaged, or full trust in MSIX |
AppContainer option |
Yes, through a packaging project’s Trust Level and the manifest |
Yes, through |
Yes, through a packaging project’s Trust Level |
Run-time XAML |
|
|
Not applicable (no XAML) |
Secrets |
DPAPI, Credential Manager, |
The same; |
The same |
Elevation |
|
Same manifest mechanism; packaged apps are normally |
|
Native AOT or trimming as hardening |
Not supported |
Not a supported way to reduce the attack surface of a XAML UI |
Not supported |
The first two rows come from the MSIX and capability documentation. The ApplicationData note is stated by the
app-data article: those APIs are designed for packaged apps, and unpackaged apps must use other storage. Trimming and
Native AOT are covered by the publishing-options page of this section; they are build-size and start-up features,
not security controls.
The Security Checklist of a CLI Book, Mapped to Desktop
Tidjani Belmansour’s Building CLI Applications with C# and .NET (Packt, 2025) devotes its thirteenth chapter to security. Its central claim is that security is not only the application’s code or its authentication but the whole lifecycle, and it walks through the stages. The book targets console applications on .NET 8, so some items need translating. The table keeps the book’s structure and states what each stage means for a Windows desktop app. This page cites the book for its checklist only; the Windows-specific controls come from the Microsoft documentation listed in the references.
| Book stage | The book’s point | Desktop translation for Bookshelf Desktop |
|---|---|---|
Design |
Define security requirements, do threat modeling (STRIDE) and prioritise with DREAD; plan privacy and data protection. |
Do the STRIDE pass at the top of this page for every new feature; decide early which parts handle untrusted data and might need the AppContainer tier. |
Architecture |
Defense in depth, secure channels, encryption at rest and in transit. |
HTTPS for every call; DPAPI or Credential Locker for the token; separate the privileged broker from the parser; a signed package as the only deliverable. |
Development |
Secure coding, input validation, OWASP Top 10, supported libraries and frameworks, code review. |
Validate imported files and view-model input (see the validation page); avoid loading untrusted XAML; stay on a supported .NET (8 and 9 end on 10 November 2026). |
Infrastructure configuration |
For CLIs this is delegated to the user or their IT department. |
For desktop apps the same is true of the device, so declare what you need (capabilities, |
Testing |
Penetration testing and vulnerability assessment before production. |
Test the denied paths (no capability, no network, revoked token); fuzz the file importers; run the app as a standard user and in an AppContainer. |
Deployment |
Protect CI/CD secrets with a key vault or secret store; restrict access with roles. |
Signing keys and tokens only from CI secrets; never in the repository; restrict who can release. |
In use |
Monitor, log, audit, patch and update regularly. |
A working update channel and a crash and telemetry policy that respects privacy; a plan to ship a security fix quickly. |
Logging |
Do not log sensitive data; if you must, protect the logs; mask properties. |
Redact tokens, passwords and personal data before they reach any log sink; keep log files in the per-user data folder. |
New versions |
Security impact analysis and regression testing for every change. |
Re-run the audit and the STRIDE pass on each release; keep security tests in the regression suite. |
The same chapter also covers three tools and ideas that map directly:
| Book item | The book’s point | Desktop equivalent |
|---|---|---|
Dependency scanning with |
A built-in way to find vulnerable and outdated NuGet packages, backed by the GitHub Advisory Database. |
NuGet Audit during restore (default on) plus the same commands; an audit stage in CI, as shown above. Outdated packages stop receiving security updates, so review them too. |
Third-party scanners (SonarQube, Snyk, Mend Bolt, OWASP Dependency-Check, GitHub Advanced Security) |
Static analysis, dependency checks and secret scanning in the pipeline. |
The same tools apply to a desktop repository; add secret scanning so that no signing material or token is committed. |
Authentication with a personal access token or an identity provider (OAuth 2.0 and OpenID Connect) |
Pass the token to the service; store it in a config file, environment variable or the operating system’s key vault rather than on the command line. |
Keep the token in DPAPI or the Credential Locker; prefer an interactive OAuth sign-in with short-lived tokens; the desktop app never needs to ask the user to type a long-lived token. |
One translation deserves emphasis. A CLI user types a token on a command line, so the book recommends not passing it as a parameter. A desktop app has no command line for end users, but it has the same underlying problem: wherever the token comes to rest on disk, an attacker running as the user can read it, which is why the operating-system stores above are preferred to files.
Pitfalls
-
Assuming MSIX means sandboxed. A packaged WinUI 3, WPF or WinForms app is full trust by default and can reach everything the user can. Declare
uap10:TrustLevel="appContainer"only after planning for the lost access. -
Believing virtualization is a security boundary. Redirected
AppDataandHKCUwrites aid cleanup; writes outside those areas still go through if the user has permission. -
Defaulting to
requireAdministrator. It prompts on every start and gives every exploit administrator rights. UseasInvokerand a separate elevated helper. -
Treating restrictive
XamlReaderas safe. It is defense in depth only, according to the API documentation. -
Storing secrets in settings or registry. Use DPAPI, the Credential Locker or a server-issued short-lived token.
-
DataProtectionScope.LocalMachinefor per-user data. Any account on the computer can decrypt it. -
Relying on an EV certificate for SmartScreen. It no longer bypasses it; reputation takes time and a consistent signing identity.
-
Letting audit warnings go unread. Run the dedicated audit pipeline and act on NU1903 and NU1904.
-
Updating over plain HTTP, or running a downloaded payload without checking its signature.
-
Logging tokens, passwords or full request bodies. Redact before logging.
-
Putting secrets in the package. Anything inside an MSIX can be read by the user and by malware running as them.
Summary
Choose the trust tier on purpose (unpackaged, packaged full trust, or AppContainer), declare the smallest execution level and capability set that works, keep secrets in an operating-system store behind an interface, treat run-time XAML as code, audit dependencies on every restore, sign every release and protect the pipeline that signs it, and re-run a short threat model whenever a feature adds an input, a file format or a network call.
References
-
Tidjani Belmansour, Building CLI Applications with C# and .NET, Packt, 2025, chapter 13, "Security Considerations for CLI Applications": https://www.packtpub.com/en-us/product/building-cli-applications-with-c-and-net-9781835882740