Skip to content

Fix Handle SF CLI 2.136+ credential redaction in SFDX org config - #4009

Merged
mgrandhi merged 12 commits into
SFDO-Tooling:mainfrom
sf-brahm-gaur:bugfix/sfdx-credential-redaction
Sep 7, 2026
Merged

Fix Handle SF CLI 2.136+ credential redaction in SFDX org config#4009
mgrandhi merged 12 commits into
SFDO-Tooling:mainfrom
sf-brahm-gaur:bugfix/sfdx-credential-redaction

Conversation

@sf-brahm-gaur

Copy link
Copy Markdown
Contributor

Handle SF CLI 2.136+ credential redaction in SFDX org config

Summary

SF CLI 2.136+ no longer returns raw secrets from sf org display --json.
Instead of the actual value (or absence), redacted fields come back as a
truthy sentinel string prefixed "[REDACTED] ", e.g.
"[REDACTED] Use 'sf org auth show-access-token' to view".

CumulusCI's SfdxOrgConfig detected redaction with if not value:, which
never fires against a truthy sentinel — so the access token and password
were silently populated with the placeholder string, later surfacing as
opaque API 401s. This PR corrects the detection model and adds the
documented fallback path.

Changes

  • Detect the [REDACTED] sentinel rather than testing for
    absence/null. sfdx_info (access token + password) and
    get_access_token all route through the new detection helper.
  • Fall back to the show-* subcommands when a field is redacted:
    sf org auth show-access-token and sf org auth show-user-password.
    The legacy-CLI path (raw values, no sentinel) is unchanged.
  • Use result.id directly for org_id instead of splitting it out of
    the access-token prefix, which is no longer available under redaction.
  • Shell-quote usernames at every --target-org={username}
    interpolation site (these run through sfdx() with shell=True),
    closing a command-injection footgun.
  • Gate the password fallback on the sentinel only, not on absence, so
    passwordless orgs (web-auth sandboxes, scratch orgs without a generated
    password) don't fire a pointless extra subprocess.

Cache shape and the 1-hour TTL are preserved. ScratchOrgConfig.scratch_info
inherits the fix via the existing alias.

Testing

  • Added coverage for the sentinel-detection and shell-quote contracts,
    including the passwordless-org "no fallback" case and the
    both-fields-redacted scratch-org shape.
  • Full suite green (test_config_expensive + test_org).

Closes #3987

@sf-brahm-gaur
sf-brahm-gaur requested a review from a team as a code owner September 7, 2026 06:33
jstvz added 12 commits September 7, 2026 12:39
Adds private helper that calls sf org auth show-access-token for use
as a fallback when sf org display redacts the token (SF CLI 2.136+).

Refs SFDO-Tooling#3987
Adds private helper that calls sf org auth show-user-password for use
as a fallback when sf org display redacts the password (SF CLI 2.136+).
Returns None on failure or absent password; never raises.

Refs SFDO-Tooling#3987
sfdx_info now uses result.id directly for org_id (no more token-prefix
split) and falls back to sf org auth show-access-token /
show-user-password when sf org display redacts those fields.

Cache shape and 1-hour TTL preserved. ScratchOrgConfig.scratch_info
inherits the fix via the existing alias.

Fixes SFDO-Tooling#3987
When sf org display does not return accessToken (CLI 2.136+ redaction),
fall back to sf org auth show-access-token. Legacy CLI path unchanged.

Refs SFDO-Tooling#3987
Pre-commit black hook on the prior task commits was skipped due to a
local Python 3.14 / black 22.3.0 incompatibility. Run black manually
to keep the test file aligned with the project format.

Refs SFDO-Tooling#3987
After sfdx_info requires result.id from `sf org display --json`, the
test_org_import fixture must include the id field. The two sibling
fixtures (test_org_import__persistent_org, test_org_import__trial_org)
already had it.
Code-reviewer pass on the original fix found two BLOCKING defects:

B0 (correctness): the CLI replaces redacted secrets with a truthy
   sentinel string prefixed "[REDACTED] ", not absent/null. A
   `if not value:` check never triggers the fallback. Three sites
   affected: sfdx_info access_token, sfdx_info password,
   get_access_token.

B1 (security): every `--target-org={username}` interpolation runs
   through sfdx() which uses shell=True. Usernames with shell
   metacharacters are an injection footgun.

These tests RED against the current fix; they pin the corrected
detection model and shell_quote contract from the round-3 spec.

- test_sfdx_info_sentinel_token_triggers_fallback: org display
  returns redacted sentinel as accessToken; expects the helper to
  fall back and return the real token.
- test_fetch_user_password_returns_none_on_sentinel: defensive
  guard if sf org auth show-user-password itself ever returns a
  sentinel value.
- test_get_access_token_sentinel_triggers_fallback: per-user
  lookup path must trigger fallback on sentinel, not just absence.
- test_fetch_access_token_shell_quotes_username: usernames with
  shell metacharacters (e.g. semicolons) must be shell_quote'd in
  the rendered command string.

Pre-commit black 22.3.0 is incompatible with the local Python 3.14
build (ast.Str removed in 3.12); SKIP=black follows the precedent
set by 71b8e13 on this same branch. The file was reformatted with
uvx black==24.10.0 before commit so CI's modern black stays clean.

The green fix lands in the next commit.
Fixes the two BLOCKING defects from code review of SFDO-Tooling#3987:

B0 (correctness): SF CLI 2.136+ replaces redacted secrets with the
truthy sentinel "[REDACTED] Use 'sf org auth show-*' to view", not
absent/null. The original `if not value:` checks never fired the
fallback. Add `_is_redacted()` keying on the "[REDACTED] " prefix
and route all three sites (sfdx_info access_token, sfdx_info
password, get_access_token) through it.

B1 (security): every `--target-org={username}` interpolation runs
through sfdx() which uses shell=True. Wrap username in
shell_quote() at the three call sites: get_access_token's own
`sf org display` line, _fetch_access_token, _fetch_user_password.

I1 (behavior): gate the password fallback subprocess on
`fallback_fired AND _is_redacted(password)`. Legacy passwordless
orgs (`password` absent on old CLI) no longer trigger a pointless
extra subprocess; they short-circuit to "no password".

Defensive: _fetch_access_token raises and _fetch_user_password
returns None when `show-*` itself ever returns a sentinel value.
Indicates a future CLI change; better to fail loud than hand back
a placeholder.

N1 (test hygiene): rename the test fixture org id from the
typo'd "OODxxxxxxxxxxxx" (letter O O D) to "00Dxxxxxxxxxxxx"
(zero zero D), matching real Salesforce org id shape across 17
call sites in cumulusci/cli/tests/test_org.py.

Pre-commit black 22.3.0 is incompatible with local Python 3.14;
SKIP=black follows the precedent set by 71b8e13. Files were
reformatted with uvx black==24.10.0 first.

Closes SFDO-Tooling#3987.
Round 3 review caught that the previous password gate fired one
extra subprocess for the most common new-CLI org class. A new
CLI returns the token sentinel + OMITS `password` entirely for a
passwordless org (web-auth sandbox, scratch org without
`force:user:password:generate`). The previous
`fallback_fired AND _is_redacted(password)` gate evaluated
True/True on every sfdx_info refresh and ran a pointless
`sf org auth show-user-password`.

Split the helper:

- `_is_sentinel(v)` -- strict prefix check, used for the password
  gate. An absent password is not a sentinel.
- `_is_redacted(v)` -- absent OR sentinel, used for the token
  path. An absent token is as actionable as a redacted one
  (both mean "ask `sf org auth show-access-token`").

Test coverage:

- `test_sfdx_info_new_cli_fetches_token` corrected: assert
  call_count == 2 (was 3) and password absent. Codifies the
  passwordless-org-no-fallback contract.
- `test_sfdx_info_sentinel_token_triggers_fallback` (round-2)
  corrected the same way.
- `test_sfdx_info_new_cli_fetches_password` rewritten: both
  fields are now sentinels (the real shape for a scratch org
  with a generated password on a new CLI), not absence.
- New `test_sfdx_info_new_cli_no_password_org`: explicit
  passwordless-org no-fallback assertion.
- New `test_sfdx_info_sentinel_password_uses_fetched`: sfdx_info-
  level coverage of the both-sentinels path. Was helper-level
  only.

Files reformatted with `uvx ruff format`. Pre-commit black 22.3.0
incompatible with local Python 3.14 (`ast.Str` removed in 3.12);
`SKIP=black` follows the precedent set by 71b8e13 + bdc2056.

119/119 tests green in test_config_expensive + test_org.
Address round-4 review comments (no blocking findings; nit polish only).

- Add load-bearing trailing-space comment to `_REDACTED_PREFIX`. The
  CLI sentinel is the literal `"[REDACTED] Use ..."` form; a
  prefix without the trailing space would also match the unrelated
  string `"[REDACTED]Use ..."` if Salesforce ever shipped that.
- Tighten `_fetch_user_password` docstring: name every failure path
  (non-zero exit, malformed JSON, sentinel result) and the policy
  (return None, never raise; debug-log to keep the call diagnosable).
- Add debug logs on both `_fetch_user_password` failure branches so
  transient subprocess / JSON failures surface in `cci --debug` runs
  without promoting the call to raise.
- Drop the `elif not password: password = None` no-op. `result.get()`
  already returns None for absent keys; the conditional was vestigial
  from the compound-gate version.
- Drop the redundant first assertion in
  `test_fetch_access_token_shell_quotes_username`. The second
  assertion is strictly stronger.
- Add `Command.call_count == 1` to `test_scratch_info` to codify
  "legacy CLI: no fallback subprocesses fire" at the test level.
- Add `Command.call_count == 3` to
  `test_sfdx_info_new_cli_fetches_password` to codify the both-
  sentinels path (org_display + show-access-token + show-user-password).

77/77 tests green in test_config_expensive. `uvx ruff check` and
`uvx ruff format --check` clean. SKIP=black per branch precedent
(71b8e13 -> bdc2056 -> 7957c49); black 22.3.0 incompatible with
local Python 3.14.
Reviewer suggestion. Flag the only quiet failure mode of the
sentinel-prefix detection in `_REDACTED_PREFIX`'s docstring:
`@salesforce/core` currently hardcodes locale to en_US (per-locale
message bundles are an unimplemented TODO upstream), so the
"[REDACTED] " literal is stable on every shipping CLI. The literal
lives inside the translatable message string, however. If Salesforce
later wires up localization, a non-English `redacted.accessToken`
could drop the prefix and silently break detection. Note the more
robust long-term signal (match sentinel message keys, or
"value is not a well-formed access token") near the constant so a
future reader sees the upgrade path without spelunking the CLI repo.

Comment-only; no behavioral change. 77/77 tests still green.
SKIP=black per branch precedent.
Reviewer add-on. The locale-fragility comment names the failure mode
but leaves the on-call engineer to infer the symptom. Add one tripwire
sentence: if `sf org display --json`'s access token ever round-trips
into an API 401 with a human-readable token value, suspect this
detection path first. Converts the comment from "here is the
theoretical risk" to "here is the symptom that means this assumption
broke."

Comment-only; no behavioral change.
@sf-brahm-gaur
sf-brahm-gaur force-pushed the bugfix/sfdx-credential-redaction branch from 49c28c8 to 928a3f6 Compare September 7, 2026 08:59

@mgrandhi mgrandhi 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.

LGTM

@mgrandhi
mgrandhi merged commit 0cbadbc into SFDO-Tooling:main Sep 7, 2026
17 of 34 checks passed
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.

SfdxOrgConfig breaks with Salesforce CLI May 27 2026 security update — migrate accessToken/password reads to new dedicated commands

3 participants