Harden remote web server security

All changes are localized to RemoteWebServer plus two small helpers; the
Phase 0 characterization tests confirm routing, JSON/Prometheus shape, and
credential pass/fail semantics are otherwise unchanged.

- Password hashing: new PasswordHasher uses PBKDF2-HMAC-SHA256 with a
  per-credential random salt (self-describing pbkdf2$iters$salt$hash).
  Verify() still accepts the legacy unsalted SHA-256 hex hash and a
  successful legacy auth transparently upgrades the stored hash, persisted
  by the view model on save/shutdown. Property renamed PasswordSHA256 ->
  PasswordHash.
- Constant-time comparison: CredentialComparer.FixedTimeEquals for the user
  name and password hash; both are evaluated fully (no && short-circuit).
- No information disclosure: POST failures return a generic message instead
  of ex.ToString(); detail is logged server-side only.
- Bind intent respected: ResolveListenerIp no longer mutates ListenerIp or
  silently falls back to all-interfaces for a specific configured address
  (auto/'?'/wildcards still bind all). A bad address now fails Start().
- CORS: removed the Access-Control-Allow-Origin '*' wildcard; common
  response headers centralized in WriteCommonHeaders.
- Prometheus: label values are escaped (EscapePrometheusLabel).

185 tests pass (was 167).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 10:35:13 -05:00
co-authored by Claude Opus 4.8
parent 463e80709f
commit 15737ad639
7 changed files with 294 additions and 28 deletions
@@ -740,7 +740,7 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable
Settings.SetValue("listenerPort", ListenerPort);
Settings.SetValue("authenticationEnabled", AuthWebServer);
Settings.SetValue("authenticationUserName", AuthWebServerUserName);
Settings.SetValue("authenticationPassword", _remoteWebServer.PasswordSHA256);
Settings.SetValue("authenticationPassword", _remoteWebServer.PasswordHash);
_remoteWebServer.Quit();
_hardwareMonitor.Dispose();
Save();
@@ -798,7 +798,7 @@ public sealed class MainWindowViewModel : ViewModelBase, IDisposable
return;
_remoteWebServer.SetPassword(plainPassword);
Settings.SetValue("authenticationPassword", _remoteWebServer.PasswordSHA256);
Settings.SetValue("authenticationPassword", _remoteWebServer.PasswordHash);
RestartWebServerIfRunning();
}