DBIO
view release on metacpan or search on metacpan
docs/adr/0025-trace-redact-bind-value-hook.md view on Meta::CPAN
# ADR 0025 â `redact_bind_value()` trace hook for sensitive bind values
- Status: accepted
- Date: 2026-06-25
- Tags: public-api, security, tracing, observability, bind-values
## Context
DBIO's tracing surface â `DBIO::Storage::DBI::_format_for_trace` and the
`trace`/`debug` callbacks it routes through â captures every executed SQL
statement together with its bind values, for live debugging and for the
post-mortem query log drivers and ops teams keep. Bind values are the
authoritative data plane of a request: column values, primary keys, search
predicates, foreign-key targets. They are also where sensitive data lives â
passwords hashed into `WHERE password = ?` predicates, session tokens carried
in `WHERE api_token = ?`, PII in `WHERE ssn = ?`, vault paths in
`WHERE credential_path = ?`.
Until now there was no DBIO-level way to redact a bind value before it lands in
the trace stream. Drivers and consumers could install their own trace callback
and post-process the formatted string, but that worked at the wrong layer: by
the time the bind value is already concatenated into the trace string, every
consumer that ever touches trace output (log shippers, error reporters, the
debug REPL, the `DBIC_TRACE` env var, the `connect_info.trace` callback chain)
must re-implement redaction in their own callback. The DDL of "what was
redacted" is duplicated across every consumer and drifts; one forgotten
consumer and the secret lands in the log.
The constraint is real: a trace hook that interferes with the bind value going
to the database is broken. The trace hook must read, transform, and return a
display-only string for the trace stream, leaving the bind value going to
`$dbh` `execute()` untouched. That is the seam this ADR blesses.
## Decision
Add a class-level `redact_bind_value` hook to `DBIO::Storage::DBI` that the
trace formatter consults for every bind value, before the value lands in the
trace stream. The hook is **display-only** â it never affects the value
passed to the database.
1. **Hook API**: a single coderef that takes the bind value and returns the
redaction-safe display string.
```perl
__PACKAGE__->redact_bind_value(sub {
my ($value) = @_;
return '[REDACTED]' if defined $value && $value =~ /secret/i;
return $value;
});
```
2. **Default identity**: out of the box, `redact_bind_value` is the identity
function (`sub { return $_[1] }`) â no behaviour change for users who do
not configure it. Traces today are byte-identical to traces tomorrow
unless the hook is set.
3. **Layer**: the hook lives on `DBIO::Storage::DBI` (the base class), via
`mk_classdata` (the established per-class hook shape in this codebase).
Inherited by every storage subclass, including `Replicated::Storage` and
every driver storage.
4. **Call site**: `_format_for_trace` calls the hook *once per bind value*,
outside the `qq{}` interpolation that builds the trace string, so the
hook's return value is rendered as a literal and cannot be parsed back
into the original. The return value is `defined` or `undef` exactly as
the hook returns it; the hook is responsible for the display shape.
5. **The bind value going to `$dbh->execute(@bind)` is untouched.** This is
documented in the hook POD and in the `execute` call site comment.
6. **A mock-only regression test (`t/storage/trace_redact.t`) locks** that
(a) the default hook is identity, (b) a custom hook is invoked once per
bind value, (c) the bind value going to `execute` is byte-identical with
and without the hook installed, (d) the rendered trace string shows the
redaction rather than the original.
( run in 0.938 second using v1.01-cache-2.11-cpan-6aa56a78535 )