Skip to content

[3.0] Strip arguments and objects from logged backtraces - #9315

Open
albertlast wants to merge 1 commit into
SimpleMachines:release-3.0from
albertlast:fix/error-log-backtrace-args
Open

[3.0] Strip arguments and objects from logged backtraces#9315
albertlast wants to merge 1 commit into
SimpleMachines:release-3.0from
albertlast:fix/error-log-backtrace-args

Conversation

@albertlast

Copy link
Copy Markdown
Collaborator

Note

This change was produced by an LLM. The code, the commit message and this
description were all written by Claude (Anthropic), driven by @albertlast. It has
not yet had human code review.

Everything stated below was verified by actually running it against a fresh
PostgreSQL install of this branch, rather than only reasoned about. Even so,
please review it as untrusted work: the diagnosis may be right while the fix is
not what SMF would prefer stylistically or architecturally.

Description

ErrorHandlerService::log() json encodes the backtrace before storing it. The
backtraces it collects itself pass DEBUG_BACKTRACE_IGNORE_ARGS, but that only
applies when no backtrace was supplied:

$backtrace = $backtrace ?? debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);

catch() always supplies one, from Throwable::getTrace(), and that keeps both the
call arguments and the bound objects. Two consequences:

1. Sensitive data reaches the error log. Anything a member submitted can end up
in smf_log_errors, including the password posted to the login form — it is simply
an argument a few frames down.

2. A caught error can escalate into an uncaught fatal. Encoding an object reads
its properties, which can throw. When it does, the new error arrives while the first
one is still being logged, and an error that would only have been logged becomes a
fatal that hides the original. Observed on a logged in page load:

Uncaught Error: Cannot access uninitialized non-nullable property
SMF\User::$username by reference in Sources/User.php:808
  #1 Sources/Utils.php(1539): json_encode(Array, 0, 512)
  #2 ErrorHandlerService.php(286): SMF\Utils::jsonEncode(Array)
  #3 ErrorHandlerService.php(185): ...->log('Cannot access u...')

Note the same line number appearing twice: the handler re-triggered the very error it
was logging. The underlying defect there is fixed separately in #9314, but the
amplification is worth fixing on its own — it turns any such error into a white page
and destroys the diagnostic.

This drops both keys from every frame before encoding. File, line, function and class
are kept, which is what the log is for.

Possibly worth a separate look

While here, this looked wrong but is not changed by this PR:

// This is how to keep the args but skip the objects.
$backtrace = $backtrace ?? debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS & DEBUG_BACKTRACE_PROVIDE_OBJECT);

DEBUG_BACKTRACE_IGNORE_ARGS is 2 and DEBUG_BACKTRACE_PROVIDE_OBJECT is 1, so
the bitwise & evaluates to 0 rather than combining the flags, and the comment does
not describe what the expression does. Left alone deliberately to keep this diff
focused, and because the sanitisation above makes it moot for safety.

How this was verified

After this change, the error that previously produced an uncaught fatal was logged as
an ordinary general error and the page still returned HTTP 200. The stored backtrace
is clean and usable — it is what located #9314:

[{"file":"...Sources/User.php","line":1476,"function":"saveBatch","class":"SMF\User","type":"::"},
 {"file":"...Sources/User.php","line":4701,"function":"save","class":"SMF\User","type":"->"},
 {"file":"...Sources/User.php","line":2931,"function":"setLastVisit","class":"SMF\User","type":"->"},
 {"file":"...Sources/Forum.php","line":565,"function":"loadMe","class":"SMF\User","type":"::"}]

Issues References (Fixes|Related|Closes)

  1. Related: [3.0] Avoid the deprecated by-reference aliases in saveBatch() #9314

ErrorHandlerService::log() json encodes the backtrace before storing it.
The backtraces it collects itself pass DEBUG_BACKTRACE_IGNORE_ARGS, but
that only applies when no backtrace was supplied:

    $backtrace = $backtrace ?? debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);

catch() supplies one, from Throwable::getTrace(), and that keeps both the
call arguments and the bound objects. Two consequences:

Anything a member submitted can end up in the error log, including the
password posted to the login form, since it is simply an argument a few
frames down.

Encoding an object reads its properties, which can throw. When it does,
the new error arrives while the first one is still being logged, and an
error that would only have been logged becomes an uncaught fatal that
hides the original. That is what turned a background task failure into a
white page for logged in members:

  Uncaught Error: Cannot access uninitialized non-nullable property
  SMF\User::$username by reference
    #1 Sources/Utils.php(1539): json_encode(Array, 0, 512)
    #2 ErrorHandlerService.php(286): SMF\Utils::jsonEncode(Array)
    #3 ErrorHandlerService.php(185): ...->log('Cannot access u...')

Drops both keys from every frame before encoding. File, line, function
and class are kept, which is what the log is for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Signed-off-by: albertlast <mathiaspapealbert@hotmail.com>
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