Skip to content

Respect #[SensitiveParameter] when reporting mapping failures - #130

Draft
janedbal wants to merge 1 commit into
masterfrom
jt-sensitive-parameter
Draft

Respect #[SensitiveParameter] when reporting mapping failures#130
janedbal wants to merge 1 commit into
masterfrom
jt-sensitive-parameter

Conversation

@janedbal

Copy link
Copy Markdown
Member

Mapping failures embed the offending value in the exception message (Expected non-empty string, got "hunter2"), and those messages typically end up in logs and in API error responses. PHP applies #[SensitiveParameter] only to stack-trace arguments, so credentials mapped through an input DTO leaked verbatim.

public function __construct(
    public readonly string $login,

    #[SensitiveParameter]
    #[AssertStringNonEmpty]
    public readonly string $password,
) {}

/password now fails with Expected non-empty string, got string (redacted), while /login keeps reporting its value as before. Path, keys and the expectation itself are preserved — only the value is dropped.

How

  • MappingFailedException builds a second, value-free reason alongside the current one; MappingFailedException::redact($e) returns an equivalent exception built from it. missingKey/extraKeys are unchanged, as keys are structural rather than data. The source exception is deliberately not kept as previous (its message holds the value); a previous that is itself a MappingFailedException is redacted recursively.
  • SensitiveInputMapperCompiler wraps the whole subtree in try { … } catch (MappingFailedException $e) { throw MappingFailedException::redact($e); }. This covers nested mappers and any third-party Assert* validator without touching the ~36 MappingFailedException::incorrect* call sites. Required one new PhpCodeBuilder::tryCatch() helper.
  • DefaultMapperCompilerFactory wraps the parameter's provider after validators but before Optional/MapDefaultValue, so an optional sensitive parameter keeps its UndefinedAwareMapperCompiler behaviour — wrapping it on the outside would have silently made the key required. addValidatorProvider() also pushes validators inside MapSensitive, so the explicit attribute behaves identically to the native one.

Notes

  • redact() is a static factory rather than an instance method: $e->redacted() trips shipmonk.missingPreviousException in every generated mapper, whereas passing the caught exception as an argument satisfies that rule honestly and matches the existing static-factory style of the class.
  • #[SensitiveParameter] is PHP 8.2+, and this package supports 8.1. Detection is by attribute name, so nothing breaks on 8.1 — the attribute just cannot be written there, hence the #[MapSensitive(new MapString())] fallback documented in the README.
  • Not addressed here: ObjectInputMapperCompiler dumps the whole payload when the input is not an array, before any per-parameter mapper runs.

Co-Authored-By: Claude Code

Mapping failures embed the offending value in the exception message,
which typically ends up in logs and in API error responses. PHP applies
#[SensitiveParameter] only to stack-trace arguments, so credentials
mapped through an input DTO leaked verbatim.

Constructor parameters marked with #[SensitiveParameter] (or wrapped in
the new #[MapSensitive]) are now compiled with a try/catch that replaces
the offending value with its type, keeping path, keys and expectation
intact. The wrapper sits below Optional/default-value handling and above
validators, so validator messages are redacted too.

Co-Authored-By: Claude Code
@janedbal
janedbal requested a review from JanTvrdik July 27, 2026 15:36
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