Skip to content

[Bug]: Login passwords can appear in nextcloud.log: OC\Log::logException merges an unredacted Normalizer copy of the exception alongside the redacted ExceptionSerializer trace #65398

Open

Description

@dodysw3

Summary

An exception logged with ['exception' => $e] ends up in nextcloud.log twice: once as the structured, redacted serialization from OC\Log\ExceptionSerializer (sensitive arguments replaced with *** sensitive parameters replaced ***), and once as a string produced by Nextcloud\LogNormalizer\Normalizer, which embeds Throwable::getTraceAsString(). PHP's text-format trace includes function arguments verbatim, and this second copy bypasses ExceptionSerializer's redaction entirely.

In practice this leaks login passwords (and other runtime secrets) into nextcloud.log whenever an exception is logged while a password-bearing frame is on the stack.

Steps to reproduce

Observed in production on 34.0.1 (steps simplified to the cleanest chain I could find; the bug itself is in the logging layer and affects any ['exception' => $e] log from a password-bearing stack):

  1. Install NC 34 with default apps (suspicious_login is shipped and enabled). Do not configure email (SMTP defaults to 127.0.0.1:25), or point mail at an unreachable host.
  2. Log in as a user from an IP the suspicious-login classifier considers a new location. OCA\SuspiciousLogin\Listener\LoginMailListener then synchronously tries to send the "New login location detected" mail inside the login request.
  3. The send fails with Symfony\Component\Mailer\Exception\TransportException, and OC\Mail\Mailer::send() logs it at error level with the full exception (lib/private/Mail/Mailer.php, the Sending mail to ... failed branch).
  4. The resulting nextcloud.log entry contains the user's plaintext password twice inside the exception.exception string field:
#24 [internal function]: OC\Server->{closure:...}(Object(OC\User\User), 'admin', '<PLAINTEXT PASSWORD>', false)
...
#39 ...: OC\Core\Controller\LoginController->tryLogin(Object(OC\Authentication\Login\Chain), Object(OC\Security\TrustedDomainHelper), 'admin', '<PLAINTEXT PASSWORD>', true, NULL, 'Europe/Berlin', '60')

while the same entry's structured exception.Trace correctly shows *** sensitive parameters replaced *** for exactly those frames.

Any other code path that logs an exception whose stack passes through a frame holding a secret (auth backends, encryption app, ...) is affected the same way — the mail-during-login chain is just the trigger I hit.

Expected behaviour

No sensitive argument values anywhere in the log entry. The redaction performed by OC\Log\ExceptionSerializer should cover all serialized forms of the exception that reach the log.

Actual behaviour

Two copies of the exception are merged into one entry:

  • exception.Trace — structured, properly redacted (OC\Log\ExceptionSerializer::filterTrace() matches sensitive methods like completeLogin/{closure}, collects the argument values, and scrubs them from the whole structured trace).
  • exception.exception — a JSON-ish string from Nextcloud\LogNormalizer\Normalizer::normalizeException(), whose trace field is $exception->getTraceAsString(), i.e. raw PHP text trace with all argument values, produced with no redaction hook.

Root cause

OC\Log::logException() (lib/private/Log.php, around line 340 on 34.0.1):

$context = array_map($this->normalizer->format(...), $context); // 'exception' => Normalizer string w/ getTraceAsString()
$data = $context;
unset($data['app'], $data['level']);

$data = array_merge($serializer->serializeException($exception), $data); // redacted structured + raw string, both kept

OC\Log\ExceptionSerializer::filterTrace() only ever sees the structured trace; the Normalizer output is merged in afterwards and ships unredacted. The same pattern exists in OC\Log::log()/logData() for arbitrary context values.

Secondary defect in the same field: Normalizer::convertToString() runs str_replace(['\\u0000', '\\\\'], ['', '\\'], ...) over the JSON-encoded string, which un-escapes backslashes inside JSON string literals — so exception.exception is invalid JSON ("class":"Symfony\Component\Mailer\...") and breaks any downstream log parser that tries to decode it (json_decode() returns null).

Suggested fix

Either of:

  1. In OC\Log::logException() (and log()/logData()), stop merging the raw Normalizer exception string into the entry — the structured ExceptionSerializer output already carries class/message/trace. This also removes the invalid-JSON field.
  2. Or expose the sensitive values collected by ExceptionSerializer::filterTrace() and use them to post-redact the Normalizer output before merging (those values already include the login password, since the frames that carry it are on the sensitive-methods list).
  3. Longer term: give Nextcloud\LogNormalizer\Normalizer an injectable argument-redaction hook so text traces can be scrubbed at the source.

Happy to attempt a PR if maintainers have a preference between these.

Impact note

Found during a real incident: a migration left SMTP unconfigured, one login produced a nextcloud.log entry containing that user's plaintext password, and log files routinely end up in backups/support bundles, which broadens exposure beyond the server itself. Reporting publicly here since exploitation requires log access; if triage prefers to handle this as a security report instead, feel free to move it.

Server configuration

  • Nextcloud version: 34.0.1 (installed version string 34.0.1.2), official Docker image nextcloud:34.0.1
  • PHP: 8.5.8
  • Bundled lib: 3rdparty/nextcloud/lognormalizer (upstream https://github.com/nextcloud/lognormalizer/), as shipped in 34.0.1

Activity

  1. pk7243959-wq commented on Oct 11, 2026

    @pk7243959-wq

    Hi @dodysw3, thanks for the detailed report!

    I can take this up using Option 1 (unsetting the unredacted $data['exception'] in OC\Log::logException() before merging so ExceptionSerializer remains the single source of truth, along with regression unit tests).

    Could this be assigned to me?

  2. added 3 commits that reference this issue on Oct 11, 2026
    3d93890
    ebd32c9
    cd353d4
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions