Skip to content

Redact secret values from conversion-failure exceptions (S1) - #27

Merged
guy-lud merged 2 commits into
masterfrom
security/s1-redact-exception-value
Jul 13, 2026
Merged

Redact secret values from conversion-failure exceptions (S1)#27
guy-lud merged 2 commits into
masterfrom
security/s1-redact-exception-value

Conversation

@guy-lud

@guy-lud guy-lud commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

S1 — Redact secret values from conversion-failure exceptions

Found during the P5 security review. A bound configuration value that fails type conversion could reach logs two ways:

  1. Our own SettingsPropertyValueException message interpolated the raw value ([{value}]).
  2. The failing converter's framework inner exception embeds the raw input in its own message (FormatException/ArgumentException for int/enum/DateTime) and was chained in, so Exception.ToString() / ILogger printed it.

Realistic trigger: a secret on a typed property (a credentialed Uri that fails to parse, or a secret mis-bound to int/enum). A secret bound to string never fails conversion, so it doesn't hit this.

Fix (full — Rec 1)

The three-specialist plan review chose the full fix and rejected an opt-in "restore diagnostics" flag as insecure-by-configuration.

  • Resources.PropertySetterExceptionMessage — no longer includes the value; reports property name, target type, and the failing converter's exception type name (a compile-time identifier, cannot carry a secret). Fixes the to to typo.
  • SettingsPropertyValueException — ctor drops the value param and no longer chains the framework inner. Invariant: never carries a value, never chains an inner → auditably leak-proof.
  • New SettingsPropertyNullException (internal) — the value-free "AllowEmpty = false, no value" path keeps its full, useful message instead of being redacted; ConvertPropertyValue rethrows it unredacted.
  • ISectionBinder — doc note that custom binders must not throw value-bearing messages (the public SettingsBindingException chains the binder's inner; the four built-ins don't).

Both changed exceptions are internalnon-breaking.

Tests

Conversion/ExceptionRedactionTests.cs (+5): binds a sentinel secret to int/enum/DateTime/Uri and to a hostile custom converter, asserting the sentinel is absent from the entire ex.ToString() chain while property name + target type remain. Existing null-path test updated to SettingsPropertyNullException + message assertion. Suite 76 net10 (was 71).

Note (modern .NET): UriFormatException is generic (no URI), so the Uri leak was via our message, not the inner — the Uri test targets our message accordingly.

Tradeoff

A non-secret misconfiguration now loses the framework message/stack (gets property + target + failure type instead). Deliberate security-over-diagnostics call — worth a release-note mention.

Review

  • Plan reviewed by security-auditor (ENDORSE-WITH-CHANGES; all 3 required changes folded in), architecture + performance lenses (off the hot path — failure-only).
  • Code reviewed via /code-review (high) + Roslyn detect_antipatterns (0) — blast radius is only the two throw sites + tests.

A bound configuration value that failed type conversion could reach logs
two ways: our own SettingsPropertyValueException message interpolated the
raw value, and the failing converter's framework inner exception
(FormatException/ArgumentException for int/enum/DateTime) embeds the raw
input and was chained in. A secret on a typed property (e.g. a credentialed
Uri, or a secret mis-bound to int/enum) therefore leaked into logs.

Fix (full — plan review chose this over redact-message-only and over an
opt-in "restore diagnostics" flag, which was rejected as insecure-by-config):

- Resources.PropertySetterExceptionMessage: no longer includes the value;
  reports property name, target type, and the failing converter's exception
  type name (a compile-time identifier that cannot carry a secret). Fixes the
  "to to" double-word typo.
- SettingsPropertyValueException: ctor drops the value parameter and no longer
  chains the framework inner exception. Invariant: this type never carries a
  value and never chains an inner, so it is auditably leak-proof.
- New SettingsPropertyNullException (internal): the "AllowEmpty = false with no
  value" path is value-free, so it keeps its full, useful message instead of
  being redacted into a value-conversion exception; ConvertPropertyValue
  rethrows it unredacted.
- ISectionBinder: doc note that custom binders must not throw exceptions whose
  message embeds a fetched value (the public SettingsBindingException chains
  the binder's inner). The built-in binders don't.

Both changed exceptions are internal, so this is non-breaking.

Tests: new Conversion/ExceptionRedactionTests.cs binds a sentinel secret to
int/enum/DateTime/Uri and to a hostile custom converter, asserting the secret
is absent from the entire exception ToString() chain while the property name
and target type remain; the existing null-not-allowed test now expects
SettingsPropertyNullException and asserts its message. Suite 76 net10 (was 71).

Also refreshes FIX-PLAN.md (S1 marked done) and SESSION-HANDOFF.md.
{
return propertyPlan.Conversion.Convert(value);
}
catch (SettingsPropertyNullException)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can say is not SettingsPropertyNullException in the catch(exception).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 3d0e9b8 — switched to catch (Exception e) when (e is not SettingsPropertyNullException). Same behavior, and the filter skips the catch entirely so the null exception's throw context is untouched (no rethrow). Thanks!

Address PR #27 review: replace the separate catch(SettingsPropertyNullException)
{ throw; } with a filter on the general catch — catch (Exception e) when
(e is not SettingsPropertyNullException). Same behavior, more concise, and the
filter never unwinds into the catch so the null exception's throw context is
untouched.
@guy-lud
guy-lud merged commit 5277c60 into master Jul 13, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant