SnoopWPF.Agent is a developer debugging tool that runs on 127.0.0.1 only. This
document describes the security controls in place and the threat model.
- Localhost only. The server is hardcoded to
127.0.0.1. No configurable hostname parameter. - Mutations disabled by default.
wpf_set_propertyreturnsMutationDisabledunlessEnableMutation = trueis explicitly set. - Sensitive property redaction. Properties whose names match security-sensitive keywords are never read — the getter is not invoked.
- Current-user isolation. Named pipes and settings files are ACL'd to the current Windows user. Injection into processes owned by other users is blocked.
Both injection mode and NuGet mode use the same pipe security model.
The pipe is created with PipeOptions.CurrentUserOnly. This causes Windows to set an
ACL that restricts the pipe to the current user only.
In injection mode this is applied in PipeConnection (host side). In NuGet mode this
is applied in McpServerSetup.RunWithPipeAsync.
The pipe name is either a random GUID (Guid.NewGuid().ToString("N")) in injection
mode, or snoop-agent-{guid} in NuGet mode. In NuGet mode the embedding application
can also supply a custom name via SnoopAgentOptions.PipeName.
A 256-bit session token is generated by RandomNumberGenerator.GetBytes(32) and
encoded as hex.
In injection mode the token is delivered to the injected DLL via the temporary
settings file. In NuGet mode it is exposed to the embedding application via
SnoopAgentHandle.SessionToken.
The token is never transmitted over the pipe in either direction. Instead, ownership is proved via a nonce-based HMAC challenge/response (protocol version 2).
The handshake sequence (server speaks first):
- Server → Client:
HandshakeChallenge— contains a fresh 16-byte cryptographically random nonce (RandomNumberGenerator.GetBytes(16)) and the server'sProtocolVersion(currently 2). A new nonce is generated for every connection, providing per-connection replay protection. - Client → Server:
HandshakeResponse— containsProofHmacandProtocolVersion. The client computes:ProofHmac = HMACSHA256(key = UTF8(sessionToken), data = nonce) - Server validates:
- Protocol version:
response.ProtocolVersionmust equalProtocolConstants.ProtocolVersion(2). - HMAC proof: server independently computes the expected HMAC and compares with
CryptographicOperations.FixedTimeEquals(constant-time comparison to prevent timing oracle attacks).
- Protocol version:
- In injection mode the server additionally verifies the client PID via
GetNamedPipeClientProcessId()before sending the challenge.
Both sides apply a 5-second timeout (ProtocolConstants.HandshakeTimeoutMs = 5000)
to prevent an unresponsive or rogue peer from blocking the host indefinitely. If the
agent does not respond within the window, the host throws SnoopErrorCode.OperationTimedOut.
The session token is never logged. Because it is used only as an HMAC key and never appears on the wire, interception of the pipe traffic does not expose it.
The temporary settings file containing the pipe name and session token is written with
an owner-only DACL (FileSecurity with SetAccessRuleProtection + single
FileSystemAccessRule for the current user's SID). The injected agent deletes the
file immediately after reading it. If the agent never runs, the host deletes the file
on exit.
Before injecting, snoop-mcp verifies that the target process is owned by the current
Windows user. It reads the target process token via OpenProcess /
OpenProcessToken / GetTokenInformation(TokenUser) and compares the SID.
If the SIDs do not match, injection is refused with a clear error message. The target
process is never touched. A --force flag can override this check (not yet
implemented in v1).
The process handle is opened with PROCESS_QUERY_INFORMATION access only and is
closed immediately after reading the token.
Properties are redacted if their name contains any of these substrings (case-insensitive):
password passwd pwd secret apikey
connectionstring connstr credential privatekey sharedkey
cookie sessionkey authorization authtoken authkey
accesstoken bearertoken refreshtoken sessiontoken sastoken jwttoken
Note: bare auth and token are not on the list — they would redact legitimate
properties like IsAuthorized and CancellationToken.
Additionally redacted regardless of name (structural type check via IsStructurallySensitive):
- All
SecureString-typed properties. - All
NetworkCredential-typed properties. - All
DbConnectionStringBuilder-typed properties. PasswordBox.Passwordis covered by the"password"keyword match above, not by a property-identity check.
The getter is not invoked for redacted properties. The value "[REDACTED]" is
returned without calling the getter, preventing any side effects.
Redacted properties also cannot be set via wpf_set_property — attempting to do so
returns PROPERTY_REDACTED.
Redaction is applied uniformly to properties, trigger condition/setter values, and behavior property values.
When mutations are enabled, wpf_set_property parses string values using a
hardcoded internal converter table only. TypeDescriptor.GetConverter() is never
called for mutation operations. This prevents app-registered custom type converters
from executing arbitrary code.
Settable types (hardcoded whitelist):
string, bool, int, double, float, decimal, long,
System.Windows.Media.Color, System.Windows.Thickness, System.Windows.GridLength,
System.Windows.CornerRadius, System.Windows.FontWeight, System.Windows.FontStyle,
System.Windows.Visibility, System.Windows.HorizontalAlignment,
System.Windows.VerticalAlignment, System.Windows.TextAlignment,
System.Windows.Point, System.Windows.Size, System.Windows.Rect,
all enum types.
Never settable (not in the whitelist):
Uri, ImageSource, BitmapSource, FontFamily, Style, ControlTemplate,
DataTemplate, Binding, Type, any UIElement subtype.
Edge case: FontWeight is parsed by looking up names on FontWeights via
reflection (GetProperty with no IgnoreCase flag). The lookup is therefore
case-sensitive: "Bold" works, "bold" does not. All other enum types use
Enum.Parse with ignoreCase: true.
The injected DLL installs an AppDomain.AssemblyResolve handler immediately on
entry. The handler only intercepts assemblies whose simple name starts with
SnoopWPF. or Snoop. (or is exactly SnoopWPF / Snoop). All other assembly
resolution requests are passed through to the CLR's default resolver. The handler is
unregistered on AppDomain.ProcessExit.
Reading a WPF property invokes its getter in the target process. Property getters can:
- Trigger lazy initialization.
- Make network requests (if the property is backed by a service).
- Change application state.
This is inherent to WPF property inspection. It is the same behavior as the Snoop GUI. Be aware that inspecting an unfamiliar application may cause observable side effects.
Sensitive values are never written to logs:
- Pipe names and session tokens are never logged, even in verbose mode.
- Settings file paths are never logged.
- Property values are never included in exception messages or logs.
- Exception messages are sanitized before logging to strip paths and tokens.
SnoopLog.txt is a plain append log written via FileInfo.AppendText. No ACL or
rotation is applied. Treat it as containing potentially sensitive payloads when the
injection launcher surfaces an exception. Deletion on next session start is the user's
responsibility in MVP; ACL + rotation is tracked for v1.1.
| Threat | Mitigation |
|---|---|
| Remote access from other machines | Server binds to 127.0.0.1 only, hardcoded |
| Local network port scanning | 127.0.0.1 only; no HTTP in injection mode (stdio only) |
| Sensitive property values exposed | Contains-match redaction; getter not invoked for redacted props; redaction covers properties, triggers, and behaviors |
| Property mutation causing damage | Disabled by default; opt-in; hardcoded converter whitelist |
| Pipe hijacking | Random/GUID name; current-user ACL; nonce+HMAC-SHA256 handshake (token never on wire); per-connection nonce replay protection; 5s handshake timeout; constant-time HMAC comparison; client PID verification (injection mode) |
| Settings file exposure | Owner-only DACL; deleted after read; token zeroed from memory |
| TypeConverter gadget attacks | Hardcoded converter table; TypeDescriptor.GetConverter() never called |
| Log leakage of secrets | Pipe names, tokens, and property values never logged |
| Injection into other users' processes | Process ownership check before injection |
| Target application crash | All Dispatcher calls wrapped in try/catch; exceptions not propagated to WPF |
| Session reuse after restart | Session token per-injection; new token on every snoop-mcp invocation |
| AssemblyResolve handler pollution | Handler filters by Snoop name prefix; unregistered on shutdown |