Skip to content

Latest commit

 

History

History
219 lines (158 loc) · 9.52 KB

File metadata and controls

219 lines (158 loc) · 9.52 KB

Security Model

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.


Core Principles

  1. Localhost only. The server is hardcoded to 127.0.0.1. No configurable hostname parameter.
  2. Mutations disabled by default. wpf_set_property returns MutationDisabled unless EnableMutation = true is explicitly set.
  3. Sensitive property redaction. Properties whose names match security-sensitive keywords are never read — the getter is not invoked.
  4. 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.

Pipe Security

Both injection mode and NuGet mode use the same pipe security model.

Named pipe ACL

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.

Random pipe name

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.

Session token handshake (v2 — nonce + HMAC)

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):

  1. Server → Client: HandshakeChallenge — contains a fresh 16-byte cryptographically random nonce (RandomNumberGenerator.GetBytes(16)) and the server's ProtocolVersion (currently 2). A new nonce is generated for every connection, providing per-connection replay protection.
  2. Client → Server: HandshakeResponse — contains ProofHmac and ProtocolVersion. The client computes:
    ProofHmac = HMACSHA256(key = UTF8(sessionToken), data = nonce)
    
  3. Server validates:
    • Protocol version: response.ProtocolVersion must equal ProtocolConstants.ProtocolVersion (2).
    • HMAC proof: server independently computes the expected HMAC and compares with CryptographicOperations.FixedTimeEquals (constant-time comparison to prevent timing oracle attacks).
  4. 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.

Settings file security (injection mode)

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.


Process Ownership Check (Injection Mode)

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.


Sensitive Property Redaction

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.Password is 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.


TypeConverter Safety (wpf_set_property)

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.


AssemblyResolve Handler (Injection Mode)

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.


Property Getter Side Effects

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.


Log Sanitization

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 Mitigations Summary

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