fix: a stack trace in a log carries no argument values - #924
Merged
blaipr merged 1 commit intoSep 17, 2026
Merged
Conversation
DatabaseHandler::update() wrote (string)$source into the Eventlog row for a Throwable, and
PHP's default Exception::__toString() embeds getTraceAsString(), which prints each frame's
argument values — measured, not assumed:
#0 Command line code(3): decryptSecret('SuperSecretMast...', 'an-account-key')
Fifteen characters of every string on the stack. The chains that throw into this sink
include the crypt and database layers and the LDAP providers, so a master password, an
account password or a bind credential can be an argument on the way to the throw point, and
the row is readable by anyone whose profile has isEvl() and can be searched and exported.
formatStackTrace() is the same trace with every argument reduced to its type, and
processException() has always used it for exactly this reason — this sink was the one that
did not. processException() had the same defect on its previous-exception branch, one line
below where it uses the safe formatter.
Two things narrow it, both worth knowing: SPException::__toString() emits no trace at all,
so the application's own exception type was never the leaky one — what arrives carrying a
trace is a RuntimeException, a PDOException or a library's own, precisely the set thrown
from inside crypt and database calls. And all 84 Throwable-sourced notifications use the
event name 'exception', which is opt-in rather than in EVENTS_FIXED.
The header each exception renders for itself is kept and only the trace is replaced, so
SPException logs exactly what it logged before, hint included.
blaipr
deleted the
fix/a-stack-trace-in-a-log-carries-no-argument-values
branch
September 17, 2026 01:16
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
DatabaseHandler::update()wrote(string)$sourceinto theEventlogrow when the event's sourcewas a
Throwable. PHP's defaultException::__toString()embedsgetTraceAsString(), which printseach frame's argument values — measured, not assumed:
Fifteen characters of every string on the stack. The call chains that throw into this sink include
the crypt and database layers and the LDAP providers —
AccountMasterPassword,UserMasterPass,CustomFieldCrypt,BaseRepository::transactionAware(), the LDAP bind — so a master password, anaccount password or a bind credential can be an argument on the way to the throw point. The row is
readable by anyone whose profile has
isEvl(), and the event log can be searched and exported.formatStackTrace()insrc/Infrastructure/Functions.phpis the same trace with every argumentreduced to its type, and
processException()has always used it for exactly this reason. Thissink is the one that did not — the sibling that already gets it right was one file away.
Two things worth knowing, both of which narrow it
SPException::__toString()emits no trace at all — it issprintf('%s: [%s]: %s (%s)', …).So the application's own exception type was never the leaky one, which is what makes this hard to
see when reading the code: what arrives here carrying a trace is a
RuntimeException, aPDOException, aTypeErroror a library's own, and that is precisely the set thrown from insidecrypt and database calls.
exceptionlogging switched on. All 84Throwable-sourced notifications use theevent name
exception, which is inLogInterface::EVENTS(opt-in) and notEVENTS_FIXED, andgetLogEvents()defaults to empty. So this is not the out-of-the-box configuration — it is acheckbox labelled
exception, with no warning attached, that an administrator ticks whiletroubleshooting.
The change
The header each exception renders for itself is kept, and only the trace is replaced.
SPExceptiontherefore logs exactly what it logged before — including its hint, which the existing
testUpdateWithSPExceptionMessagepins unchanged — while everything else keeps itsClass: message in file:lineheader and gets the typed trace.processException()had the same defect in its previous-exception branch: the line above it usesformatStackTrace(), and that one called$previous->getTraceAsString(). Same fix, one line. Itwrites to
var/syspass.lograther than the database, so the audience differs, but the leak is thesame.
Test
testALoggedExceptionCarriesNoArgumentValuesthrows from a closure taking a secret, and asserts thestored description contains neither the secret nor the second argument, and still contains the
message and the word
String— arguments recorded by type, because withholding them would otherwisehave been achieved just as well by logging nothing.
It pins
zend.exception_ignore_argsfirst: whether a trace carries arguments at all is an inisetting that differs between a development build and a production one, so without pinning it the
test passes locally and proves nothing where the production ini is in force.
FunctionsTestalreadydoes this, which is where the technique came from.
Mutation-verified: reverting
src/fails it on the real trace.