Skip to content

fix(redis): do not log the Redis password - #1316

Open
bakiburakogun wants to merge 1 commit into
nextcloud:mainfrom
bakiburakogun:fix/redact-redis-credentials
Open

bakiburakogun wants to merge 1 commit into
nextcloud:mainfrom
bakiburakogun:fix/redact-redis-credentials

Conversation

@bakiburakogun

Copy link
Copy Markdown

The problem

RedisAdapter.createRedisClient() logs the configured Redis URL verbatim every time the websocket server starts. On any deployment that authenticates to Redis, that writes the password into the journal in clear text:

Creating Redis client with URL: redis://:s3cret@10.0.0.5:6379

We hit this on a production install: the password we had just put in a 0600 environment file showed up in journalctl a second later, where it is readable by anyone who can read the journal and by whatever ships logs off the box.

The change

Redact the credentials before logging. The host and port, which are the useful part of the line, stay visible:

Creating Redis client with URL: redis://:***@10.0.0.5:6379

A URL that cannot be parsed logs a placeholder rather than the raw string, so a malformed value with a secret in it does not leak either.

Testing

redis://:s3cret@10.0.0.5:6379          -> redis://:***@10.0.0.5:6379
redis://user:pass@host:6379/2          -> redis://***:***@host:6379/2
redis://10.0.0.5:6379                  -> redis://10.0.0.5:6379   (unchanged)
unix:///var/run/redis.sock             -> unix:///var/run/redis.sock (unchanged)
not-a-url                              -> <unparsable REDIS_URL>

@github-actions

Copy link
Copy Markdown
Contributor

Hello there,
Thank you so much for taking the time and effort to create a pull request to our Nextcloud project.

We hope that the review process is going smooth and is helpful for you. We want to ensure your pull request is reviewed to your satisfaction. If you have a moment, our community management team would very much appreciate your feedback on your experience with this PR review process.

Your feedback is valuable to us as we continuously strive to improve our community developer experience. Please take a moment to complete our short survey by clicking on the following link: https://cloud.nextcloud.com/apps/forms/s/i9Ago4EQRZ7TWxjfmeEpPkf6

Thank you for contributing to Nextcloud and we hope to hear from you soon!

(If you believe you should not receive this message, you can add yourself to the blocklist.)

@hweihwang hweihwang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for fixing this. The valid URL cases work. I left one comment on malformed URLs: that leak predates this PR, but the startup error still exposes the password.

Please add tests for the documented examples, encoded credentials, and startup errors. Keep the list of Redis URLs in #1318 covered too.

return parsed.toString()
} catch {
// not a parsable URL: log nothing rather than risk leaking a secret
return '<unparsable REDIS_URL>'

@hweihwang hweihwang Sep 16, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With redis://:test-password@, this helper hides the URL, but the next new URL(Config.REDIS_URL) throws an error containing the original value in input. main() then logs it, including the password.

Please return a configuration error without the raw URL and test the startup log, not just this helper.

The websocket server logs its Redis URL verbatim on every start, so a
deployment that authenticates to Redis writes the password into the journal
in clear text:

    Creating Redis client with URL: redis://:s3cret@10.0.0.5:6379

Redact the credentials before logging. The host and port, which are what the
line is useful for, stay visible.

Redacting the log line is not enough on its own. The statement after it parses
the same value with new URL(), and the error that throws carries the value it
was given on its input property, which main() prints when the server fails to
start. Raise a configuration error that repeats nothing of the value instead.

A comma is legal inside a Redis password, so the value is only read as several
node URLs when every part on its own is a URL with a scheme and a host. That
leaves redis://:pass,word@host alone while still redacting each node of the
seed list used for Redis Cluster.

Signed-off-by: Baki Burak Öğün <63836730+bakiburakogun@users.noreply.github.com>
@bakiburakogun
bakiburakogun force-pushed the fix/redact-redis-credentials branch from 3e240e6 to 1022dca Compare September 16, 2026 07:32
@bakiburakogun

Copy link
Copy Markdown
Author

You are right, and the hole was next to the one I closed rather than in it. new URL() hangs the value it was given on the error it throws, and main() prints that error, so the password came out through the crash path:

Failed to start server: TypeError: Invalid URL
    at new URL (node:internal/url:818:25)
  { code: 'ERR_INVALID_URL', input: 'redis://:test-password@' }

createRedisClient() now parses through a helper that raises REDIS_URL is not a valid URL and carries nothing from the configured value — no cause, so there is nothing to walk back to.

On the list of Redis URLs: I had to be careful there, because a comma is legal inside a password. redis://:test,password@localhost:6379 is a valid single node whose password is test,password, and splitting on every comma would have broken it in the redaction too. The value is only read as a list when it has more than one part and every part on its own is a URL with a scheme and a host. That is in splitRedisUrls() here, and #1318 will use the same helper to decide whether to build a cluster client rather than the includes(',') check you commented on there.

Tests are in tests/integration/redisUrlCredentials.spec.mjs, 29 of them:

  • the two examples written down in .env.example and the README, plus rediss://, a unix socket, username only and password only;
  • percent encoded credentials;
  • the seed node list, with and without credentials on the first node;
  • a comma inside the password and a comma inside the username, asserted to stay in one piece;
  • the startup path rather than the helper: with a malformed URL, that the thrown error has no input, that util.inspect() of it does not contain the password, and that the line the server logs says <unparsable REDIS_URL>; with a valid one, that the logged line is the redacted URL;
  • a sweep asserting the password never appears in the output for any of the shapes above.

Verified against the branch with the fix reverted: 11 of the 29 fail, including the malformed URL one. The full suite is 102 passing across 13 files, and eslint is clean on both files — note it was not before, since the two one line ifs I had written put the file two over its curly budget in eslint-suppressions.json. They have braces now.

Branch rebased onto current main, still one commit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants