Skip to content

fix(cloud): disclose + confirm login target URL to block config phishing (#400) - #401

Merged
kevinkod merged 1 commit into
mainfrom
feat/cli-login-url-disclosure-400
Jun 21, 2026
Merged

fix(cloud): disclose + confirm login target URL to block config phishing (#400)#401
kevinkod merged 1 commit into
mainfrom
feat/cli-login-url-disclosure-400

Conversation

@kevinkod

Copy link
Copy Markdown
Contributor

Closes #400.

Threat

A malicious .specnaut/backlog-config.yml (committed to a repo a victim clones) sets api_url: to an attacker https:// host. specnaut cloud login would then silently run the device/browser flow against it — a phishing vector. (The read commands token/orgs/board were already safe: credentials are keyed by apiUrl, so an unknown/attacker URL yields no stored token → "not authenticated", token never sent.)

Fix (in the shared resolution path)

  • resolveApiUrl now returns { url, source } where source ∈ {flag, config, prompt}.
  • runLogin discloses the target before opening the browser:
    Connecting to:  https://…
    Source:         project config (.specnaut/backlog-config.yml)
    
  • An explicit y/N confirmation fires ONLY on the precise phishing windowsource === "config" and no stored creds for that URL (first-ever auth against a config-supplied host). --api-url, an interactively-typed URL, and re-login to a known deployment proceed unprompted, so the init → login happy path keeps no friction. Default-deny (empty/EOF → cancelled, login() never called).

Two pure helpers (urlSourceLabel, loginNeedsTrustConfirm) carry the logic and are unit-tested.

Review

  • Architect designed it against the code: keyed-store reality means disclosure beats a blanket gate; no CredentialStore.list() (OS keychains can't enumerate via FFI).
  • security-auditor: primary phishing window closed, 0 critical/high, no bypass found, confirm is default-deny, refactor introduces no regression. One residual Medium — re-login to a URL the user already confirmed once skips the re-confirm (the disclosure line still renders every time) — acknowledged in-code; acceptable for the threat model.

Out of scope / residual

The config-URL-swap-after-prior-auth case (requires the user to have already approved that URL once) is documented in the code comment; not addressed here to avoid scope creep.

Verification

deno task lint + deno task check clean · deno task test 1036/1036 · smoke of cloud token/logout URL resolution ✓

Agent adoption

specnaut cloud login (and the specnaut login alias) now prints the target server URL and where it came from before opening the browser, and asks for confirmation the first time you authenticate against a URL that came from a project's .specnaut/backlog-config.yml. No action needed for normal use; it's a security disclosure.

Check whether any docs or onboarding notes in my project describe the
`specnaut cloud login` flow (in README.md, AGENTS.md, CLAUDE.md, or
.claude/**). If they do, add a one-line note that login now shows the target
server URL + source and confirms first-time auth against a config-supplied
URL. Open a PR with the change.

🤖 Generated with Claude Code

…ing (#400)

A malicious `.specnaut/backlog-config.yml` could point `api_url` at an attacker
HTTPS host; `specnaut cloud login` would then silently run the device/browser
flow against it. (Read commands — token/orgs/board — were already safe: creds
are keyed by apiUrl, so an unknown URL yields no token.)

Hardening, in the shared resolution path:
- `resolveApiUrl` now returns `{ url, source }` (flag | config | prompt).
- `runLogin` discloses the target before opening the browser:
    Connecting to:  <url>
    Source:         <where it came from>
- An explicit y/N confirmation is required ONLY for the precise phishing window
  — `source === "config"` AND no stored creds for that URL (first-ever auth
  against a config-supplied host). `--api-url`, a typed prompt, and re-login to
  a known deployment stay friction-free. Default-deny (empty/EOF → cancel).

Design reviewed by the architect (keyed-store reality → disclosure over a
blanket gate; no CredentialStore.list() since keychains can't enumerate).
Verified by security-auditor: primary phishing window closed, 0 critical/high,
no bypass. Residual (re-login to a URL already confirmed once) is acknowledged
in-code; disclosure still renders.

2 pure-helper unit tests. deno lint/check clean, 1036/1036 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@kevinkod
kevinkod merged commit 755fc32 into main Jun 21, 2026
8 checks passed
@kevinkod
kevinkod deleted the feat/cli-login-url-disclosure-400 branch June 21, 2026 16:52
kevinkod added a commit that referenced this pull request Aug 4, 2026
…ing (#400) (#401)

Co-authored-by: Claude Opus 4.8 <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.

harden cloud auth against config-driven token redirect (api_url)

1 participant