Skip to content

harden(forge-core/oauth): sanitize credential-store keys against path traversal (defense-in-depth, follow-up to #518) #519

Description

@initializ-mk

Defense-in-depth follow-up to the review on #518. That PR closed the reachable vector (forge mcp login now validates the server name before it reaches the store), so this is not a reachable bug — it's hardening the credential store so it stays safe regardless of what any future caller passes.

Context

The plaintext credential helpers in forge-core/llm/oauth/store.go build the file path by joining a caller-supplied provider/key directly:

  • recordPath(key) / saveRecordPlaintext(key, …) / removeRecordFile(key) → filepath.Join(dir, key+".json")
  • savePlaintext(provider, …) / loadPlaintext(provider) / removePlaintextFile(provider) → filepath.Join(dir, provider+".json")

And forge-core/mcp/oauth_flow.go's storeKey(name) just prefixes mcp_ (which doesn't neutralize ..). None of these sanitize the key, so a caller that passes an unsanitized key with path separators or .. could read/write/delete a *.json file outside ~/.forge/credentials. Today the only callers (LLM provider OAuth, MCP login via #518's validated name) supply safe keys, but the store layer shouldn't depend on every caller getting that right.

Proposal

Make the store self-protecting at its boundary:

  1. Route every plaintext helper through a single credentialFilePath(key) (string, error) that:
    • rejects keys containing /, \, or .. (or restricts to a safe charset), and
    • defensively verifies the resolved, filepath.Clean-ed path stays within DefaultCredentialsDir() (prefix check) before any read/write/delete.
  2. Apply it in recordPath, savePlaintext, loadPlaintext, removePlaintextFile, saveRecordPlaintext, removeRecordFile.
  3. (Optional) validate the key in SaveCredentials/LoadCredentials/SaveRecord/DeleteRecord too, so callers get a clear error rather than a silently-clamped path.

The encrypted store (secrets.enc) keys aren't filesystem paths, so traversal doesn't apply there; a key-charset check is still nice-to-have for consistency but lower priority.

Acceptance criteria

  • A key/provider containing /, \, or .. is rejected by the plaintext Save/Load/Delete helpers (tokens and records).
  • The resolved plaintext path is asserted to be within ~/.forge/credentials.
  • Valid keys (existing LLM provider + mcp_<name> tokens) round-trip unchanged.
  • Unit tests with traversal-y keys assert rejection / containment.

Touch points

  • forge-core/llm/oauth/store.go (path builders + optional key validation in the public helpers)
  • forge-core/mcp/oauth_flow.go (storeKey — optional)

Relationship

Complements #518 (which validated the CLI-supplied name). No dependency; independently mergeable.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestforge-coreAffects the forge-core library (runtime, security, types, llm, mcp, auth)securitySecurity vulnerability fixes

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions