Skip to content

[Code Quality] Two parallel ServiceDto validators with different rule sets — XML/JSON imports skip upper-bound checks that CLI install enforces #898

@Christophe-Rogiers

Description

@Christophe-Rogiers

Severity: Warning (latent — validation asymmetry between CLI install and Import flows; imported XML/JSON can persist values that the CLI flatly rejects)

Files:

  • src/Servy.Core/Helpers/ServiceValidator.cs (the short validator — ServiceValidator.ValidateDto)
  • src/Servy.Core/Validators/ServiceValidationRules.cs (the full validator — ServiceValidationRules.Validate)
  • Callers:
    • src/Servy.Core/Services/XmlServiceValidator.cs:77 — calls the short one
    • src/Servy.Core/Services/JsonServiceValidator.cs:67 — calls the short one
    • src/Servy.CLI/Validators/ServiceInstallValidator.cs:47 — calls the full one
    • The Manager's ServiceConfigurationValidator also routes through the full one

What's wrong: The codebase has two ServiceDto validators with overlapping but materially different rule sets. The short one — ServiceValidator.ValidateDto — is what runs on every imported XML/JSON file (via XmlServiceValidator.TryValidate and JsonServiceValidator.TryValidate, both of which delegate to it). The full one — ServiceValidationRules.Validate — is what runs when a user installs the same service via the CLI or the Manager UI. They disagree on several bounds, in directions that matter operationally:

Field ServiceValidator.ValidateDto (Import path) ServiceValidationRules.Validate (Install path)
RotationSize upper bound ❌ unchecked (only < MinRotationSize if SizeRotation on) ✓ checks > AppConfig.MaxRotationSize (10 GB cap)
MaxRotations ❌ unchecked ✓ checks [MinMaxRotations, MaxMaxRotations]
HeartbeatInterval upper ❌ unchecked (only < MinHeartbeatInterval) ✓ checks > AppConfig.MaxHeartbeatInterval (24 h cap)
MaxFailedChecks ❌ unchecked ✓ checks [MinMaxFailedChecks, MaxMaxFailedChecks]
MaxRestartAttempts ❌ only < 0 check ✓ checks [MinMaxRestartAttempts, MaxMaxRestartAttempts]
PreLaunchTimeoutSeconds ❌ unchecked ✓ checks [MinPreLaunchTimeoutSeconds, MaxPreLaunchTimeoutSeconds]
PreLaunchRetryAttempts ❌ unchecked ✓ checks [MinPreLaunchRetryAttempts, MaxPreLaunchRetryAttempts]
PreStopTimeoutSeconds ❌ unchecked ✓ checks [MinPreStopTimeoutSeconds, MaxPreStopTimeoutSeconds]
Pre/Post-launch paths ❌ unchecked ✓ all four paths + working dirs validated
Pre/Post-stop paths ❌ unchecked ✓ all four paths + working dirs validated
FailureProgram path ❌ unchecked ✓ validated
StdoutPath / StderrPath ❌ unchecked Helper.IsValidPath + CreateParentDirectory
EnvironmentVariables format ❌ unchecked EnvironmentVariablesValidator.Validate (escape rules, key uniqueness, etc.)
ServiceDependencies format ❌ unchecked ServiceDependenciesValidator.Validate
Service name path chars ❌ unchecked ✓ rejects \ or / in service name
Credential validation ❌ unchecked NativeMethods.ValidateCredentials against LSA

Concrete reproducible scenarios:

  1. Out-of-range heartbeat survives import. Hand-craft a service.xml with <HeartbeatInterval>9999999</HeartbeatInterval> and run servy-cli import --config xml --path service.xml. The import succeeds (only < MinHeartbeatInterval is checked). The same service installed via servy-cli install --heartbeatInterval=9999999 ... is rejected by the install validator with Msg_InvalidHeartbeatInterval. The import path silently bypasses the limit, persisting an unrunnable configuration into the SQLite DB. The mismatch only surfaces later when the service tries to start.

  2. Pre-launch path with .. traversal survives import. A crafted XML with <PreLaunchExecutablePath>C:\..\..\path\to\evil.exe</PreLaunchExecutablePath> is not validated by the short validator (which only checks dto.ExecutablePath), so the malformed path persists. CLI install would have caught it via _processHelper.ValidatePath.

  3. Crafted env-var string survives import. A malformed --envVars like KEY= (no value) or unbalanced escapes is rejected by ServiceValidationRules.Validate (calls EnvironmentVariablesValidator.Validate), but goes through unchecked on import.

The defense-in-depth from JsonSecurity.UntrustedDataSettings and the XXE-prohibit settings in the XML serializer protect against deserialization attacks, but they do not protect against semantically-out-of-range data once the DTO is alive.

Suggested fix: Have XmlServiceValidator.TryValidate and JsonServiceValidator.TryValidate call the full ServiceValidationRules.Validate(dto) instead of (or in addition to) ServiceValidator.ValidateDto. Concretely, both of these classes already receive an IProcessHelper in their constructor — pass an IServiceValidationRules instead (or alongside) and replace the call:

// XmlServiceValidator.cs:77
//   was:  var validation = ServiceValidator.ValidateDto(dto);
//   ↓
var validation = _serviceValidationRules.Validate(dto);
if (validation.Errors.Any() || validation.Warnings.Any())
{
    errorMessage = string.Join("\n", validation.Errors.Concat(validation.Warnings));
    return false;
}

After that, ServiceValidator.ValidateDto is dead code and can be deleted, eliminating the drift permanently.

(This pairs with #832 — same import flow, different drift. Fixing both would consolidate the import path on the same DI-wired full validator the install path already uses.)

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions