Skip to content

Prevent disclosure of Access Tokens in logs - #6

Open
negative-video wants to merge 3 commits into
YeomansIII:mainfrom
negative-video:security/never-log-secrets
Open

Prevent disclosure of Access Tokens in logs#6
negative-video wants to merge 3 commits into
YeomansIII:mainfrom
negative-video:security/never-log-secrets

Conversation

@negative-video

Copy link
Copy Markdown

As of current, the token-exchange response is logged in full, which the next two lines parse accessToken and refreshToken out of:

  • gigya_auth.py:872 — logged at DEBUG
  • gigya_auth.py:891 — same, logged at ERROR which is on by default and needs no flag

So asking a user for a debug log to diagnose a problem gets you their live refresh token, and the ERROR path can print an access token with no debug logging enabled at all. finalizeTFA and finalizeRegistration likewise dump whole Gigya bodies (regToken, id_token, sessionInfo.login_token) on their error paths.

Separately, the CLI's -vv uses logging.basicConfig, which raises the root logger and Gigya passes regToken, gigyaAssertion, phvToken and the TFA code as URL query parameters, so third-party HTTP debug logging prints them in the clear.

This logs status, error codes, callId and field names instead of bodies, obfuscates the login email, and scopes CLI verbosity to the dompower logger.

Each leak has a test asserting the secret is absent and a diagnostic still present; all fail without the fix. ruff, mypy --strict and the suite pass.

I'm not taking credit for this discovery or fix, I stumbled across it during some testing with Claude Code.

negative-video and others added 3 commits August 12, 2026 10:47
The Gigya authentication flow logged whole API response bodies, which are
exactly the objects the surrounding code parses live credentials out of.

The worst two were in the Dominion token exchange:

  - "Token exchange response: %s" logged the body holding the live
    accessToken and refreshToken at DEBUG. Home Assistant users are
    routinely asked to enable debug logging and paste the result into a
    GitHub issue, which would publish a working refresh token - the
    long-lived one. That is full account takeover.

  - "Token exchange missing tokens. Response: %s" logged the same body at
    ERROR, visible at default verbosity with no debug flag required. It
    triggers when *either* token is missing, so a response carrying a valid
    accessToken and an empty refreshToken printed a live access token into
    every user's ordinary log.

The same pattern appeared elsewhere: the non-200 token-exchange branch
logged the raw error body and embedded it in the exception message that
Home Assistant surfaces in its UI, and the finalizeTFA and
finalizeRegistration error branches dumped whole Gigya bodies containing
regToken, id_token, sessionInfo.login_token, profile, emails and loginIDs.
Those two fire only when errorCode != 0 - precisely when a user is about
to file an issue. Credential submission also logged the account's raw
email address.

Log only what is provably non-secret and still diagnostic: HTTP status,
errorCode, errorMessage, callId, and the presence or field names of the
expected fields. This matches the style the file already used for
assertion lengths and TFATarget.obfuscated. The account email is now
obfuscated to u***@e***.com so accounts stay distinguishable in a log.

Nothing but logging changes: no wire changes, no control-flow changes, no
new exceptions, and the finalizeTFA/finalizeRegistration error handling
still falls through exactly as before.

The new tests drive each path with recognisable sentinel credentials and
assert both that no sentinel reaches caplog.text and that useful
diagnostics still do, so the fix cannot regress into a silent logger. All
six fail against the pre-change code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`-vv` called logging.basicConfig(level=DEBUG), which sets the level on the
*root* logger and so turns on DEBUG for every third-party library in the
process as well as for dompower.

That is a credential leak in its own right, independent of what dompower
itself records. The Gigya flow passes regToken, gigyaAssertion, phvToken
and the user's TFA code as URL *query parameters*, so any HTTP library
that logs full request URLs at DEBUG prints them all in the clear. A user
told to "run with -vv and paste the output" would publish a live TFA
session.

Keep the root logger at WARNING and set the level on the "dompower"
logger instead. Records from dompower still reach the root handler by
propagation, so -v/-vv are unchanged from the user's point of view:

  -      dompower WARNING, third-party WARNING
  -v     dompower INFO,    third-party WARNING
  -vv    dompower DEBUG,   third-party WARNING

This CLI's own records went through the root logger via bare
logging.info()/logging.debug() calls, which the new configuration would
have silenced at -v. They now go through a module logger, a child of
"dompower", so they follow the verbosity flag exactly as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
These two tests asserted the literal string "finalizeTFA failed" /
"finalizeRegistration failed". That phrasing is an implementation detail:
a non-zero errorCode that still carries a session is legitimately logged
as a warning that the step continued, not as a failure.

Pinning the wording makes the tests fail when combined with any change
that refines it, even though the property under test - that no secret
reaches the log, and that the error code, message and callId still do -
is fully intact. Assert on the step name and those details instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.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