Skip to content

Harden accounts CLI: markup escaping, accountId validation, selections lock#1

Merged
StuartMeeks merged 1 commit intomainfrom
release/0.6.1
May 3, 2026
Merged

Harden accounts CLI: markup escaping, accountId validation, selections lock#1
StuartMeeks merged 1 commit intomainfrom
release/0.6.1

Conversation

@StuartMeeks
Copy link
Copy Markdown
Owner

@StuartMeeks StuartMeeks commented May 3, 2026

Summary

Six fixes from a recent code review of 0.6.0, bundled into 0.6.1.

  • Escape user/provider-controlled strings in every accounts command (provider names, account names, environments, summary DisplayFields, exception messages, the --provider flag echo). Closes a markup-injection vector where a credential name or JsonException/IO message containing [ or ] garbled the table or error line.
  • Validate accountId is a GUID before any filesystem call on Delete / Select / GetCredentialByIdAsync (file backend + Keychain + libsecret). Non-GUIDs resolve to deterministic false on the mutating paths and ArgumentException on the strict read API. Closes a path-traversal / glob-injection surface where * or ..\..\foo could have flowed into Path.Combine / Directory.GetFiles.
  • CommandFormatting.ShortId safely abbreviates ids so accounts delete abc (or any short user-supplied id) no longer crashes on the [..8] slice in the confirm prompt before reaching "not found".
  • Cross-process lock around selections.json RMW via a new SelectionsLock helper (FileShare.None + FileOptions.DeleteOnClose + ~5s exponential-backoff retry). Atomic rename was already preventing torn files; this closes the lost-update window where two CLI invocations modifying different providers could lose one provider's selection. Keychain/libsecret backends are unaffected — both OS stores serialise their own writes.
  • PackageOutputPath was hard-coded as C:\nuget-local\, which on Linux produced surprising src/.../C:/nuget-local/… repo churn. Now repo-relative under artifacts/packages (already gitignored).
  • GeneratePackageOnBuild now scoped to Release builds — dotnet test and Debug builds no longer pack.

Drive-by fix: the unbalanced parenthesis in the accounts select / delete choice prompts ({c.AccountId[..8]}... was missing its closing )) — surfaced while replacing the unsafe [..8] slices.

Test plan

  • dotnet build clean (0 warnings, TreatWarningsAsErrors on)
  • dotnet test — full suite green (145 tests, +18 new)
    • CommandFormatting.ShortId across full GUIDs, short strings, exact-8 boundary, null/empty
    • Non-GUID accountId resolves to deterministic not-found on file-backend Delete/Select; throws on GetCredentialByIdAsync
    • Concurrent SelectCredentialAsync on different providers both persist (regression test for the selections.json race)
    • SelectionsLock uncontended / contended / post-release semantics
  • CI green on ubuntu-latest (libsecret) and macos-latest (Keychain)

Notes

  • No public API surface change — this is additive hardening on top of 0.6.0.
  • CHANGELOG entry added under [0.6.1] — 2026-05-03.

…s lock

- Escape user/provider-controlled strings (provider names, account names,
  environments, summary DisplayFields, exception messages) before they
  reach Spectre MarkupLine, Table.AddRow, or selection-prompt choices.
  Closes a markup-injection vector where credential names or exception
  messages containing '[' / ']' garbled command output.

- Validate accountId is a GUID before any filesystem call in
  FileCredentialManager.Delete/Select/GetCredentialById and the matching
  Keychain/libsecret APIs. Non-GUIDs now resolve to deterministic 'false'
  on the mutating paths and ArgumentException on GetCredentialByIdAsync,
  closing a path-traversal/glob-injection surface where '*' or '..\\..\\foo'
  could have flowed into Path.Combine and Directory.GetFiles.

- Add CommandFormatting.ShortId to abbreviate ids safely, so
  'accounts delete abc' (or any short user-supplied id) no longer crashes
  on the [..8] slice in the confirm prompt before reaching 'not found'.

- Serialise selections.json read-modify-write across processes via a new
  SelectionsLock helper (FileShare.None + FileOptions.DeleteOnClose with
  ~5s exponential-backoff retry). Atomic rename was already preventing
  torn files; this closes the lost-update window where two CLI invocations
  modifying different providers could lose one provider's selection.

- csproj: PackageOutputPath was hard-coded to C:\\nuget-local\\ which on
  Linux produced surprising 'src/.../C:/nuget-local/' repo churn. Now
  repo-relative under artifacts/packages (already gitignored).
  GeneratePackageOnBuild now scoped to Release builds so 'dotnet test'
  and Debug builds no longer pack.

Drive-by: fixed unbalanced parenthesis in 'accounts select' / 'delete'
choice prompts that surfaced while replacing the unsafe [..8] slices.

18 new tests; suite at 145. Bumps version to 0.6.1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@StuartMeeks StuartMeeks merged commit 93499f2 into main May 3, 2026
3 checks passed
@StuartMeeks StuartMeeks deleted the release/0.6.1 branch May 3, 2026 11:16
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.

2 participants