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:
-
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.
-
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.
-
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.)
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)src/Servy.Core/Services/XmlServiceValidator.cs:77— calls the short onesrc/Servy.Core/Services/JsonServiceValidator.cs:67— calls the short onesrc/Servy.CLI/Validators/ServiceInstallValidator.cs:47— calls the full oneServiceConfigurationValidatoralso routes through the full oneWhat's wrong: The codebase has two
ServiceDtovalidators with overlapping but materially different rule sets. The short one —ServiceValidator.ValidateDto— is what runs on every imported XML/JSON file (viaXmlServiceValidator.TryValidateandJsonServiceValidator.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:ServiceValidator.ValidateDto(Import path)ServiceValidationRules.Validate(Install path)RotationSizeupper bound< MinRotationSizeif SizeRotation on)> AppConfig.MaxRotationSize(10 GB cap)MaxRotations[MinMaxRotations, MaxMaxRotations]HeartbeatIntervalupper< MinHeartbeatInterval)> AppConfig.MaxHeartbeatInterval(24 h cap)MaxFailedChecks[MinMaxFailedChecks, MaxMaxFailedChecks]MaxRestartAttempts< 0check[MinMaxRestartAttempts, MaxMaxRestartAttempts]PreLaunchTimeoutSeconds[MinPreLaunchTimeoutSeconds, MaxPreLaunchTimeoutSeconds]PreLaunchRetryAttempts[MinPreLaunchRetryAttempts, MaxPreLaunchRetryAttempts]PreStopTimeoutSeconds[MinPreStopTimeoutSeconds, MaxPreStopTimeoutSeconds]Helper.IsValidPath+CreateParentDirectoryEnvironmentVariablesValidator.Validate(escape rules, key uniqueness, etc.)ServiceDependenciesValidator.Validate\or/in service nameNativeMethods.ValidateCredentialsagainst LSAConcrete reproducible scenarios:
Out-of-range heartbeat survives import. Hand-craft a
service.xmlwith<HeartbeatInterval>9999999</HeartbeatInterval>and runservy-cli import --config xml --path service.xml. The import succeeds (only< MinHeartbeatIntervalis checked). The same service installed viaservy-cli install --heartbeatInterval=9999999 ...is rejected by the install validator withMsg_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.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 checksdto.ExecutablePath), so the malformed path persists. CLI install would have caught it via_processHelper.ValidatePath.Crafted env-var string survives import. A malformed
--envVarslikeKEY=(no value) or unbalanced escapes is rejected byServiceValidationRules.Validate(callsEnvironmentVariablesValidator.Validate), but goes through unchecked on import.The defense-in-depth from
JsonSecurity.UntrustedDataSettingsand 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.TryValidateandJsonServiceValidator.TryValidatecall the fullServiceValidationRules.Validate(dto)instead of (or in addition to)ServiceValidator.ValidateDto. Concretely, both of these classes already receive anIProcessHelperin their constructor — pass anIServiceValidationRulesinstead (or alongside) and replace the call:After that,
ServiceValidator.ValidateDtois 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.)