MCP client setup exposes Fly API tokens to other local users
A long-lived Fly bearer token was serialized into a client config created with ambient file permissions
CVSS 3.1 base score
HighVector: CVSS:3.1/AV:L/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H
Summary
Source
runServer()
internal/command/mcp/server.go:132
Sink
UpdateConfig()
internal/command/mcp/config.go:570
The stream/SSE MCP setup command placed the user's reusable Fly API token in a client configuration argument list. On a normal Unix umask 022, the config could be created or replaced as mode 0644, allowing another local account to read the credential.
Root cause. runServer() appended --bearer-token to the persisted MCP proxy arguments and UpdateConfig() used ordinary file creation/rename semantics without enforcing an owner-only mode on the final file.
Remediation evidence. Fly opened PR #5192 the day after the disclosure and merged it on 8 September 2026. The exact patch writes new MCP configuration at 0600, repairs permissive existing files during replacement, and includes Unix permission regression tests.
Severity
- Attack vectorAV
- Local
- Attack complexityAC
- Low
- Privileges requiredPR
- Low
- User interactionUI
- Required
- ScopeS
- Changed
- ConfidentialityC
- High
- IntegrityI
- High
- AvailabilityA
- High
Metric values as published in the disclosure vector. Meters show how far each value raises exposure.
Source-to-sink trace
- Source · attacker-controlledinternal/
command/ mcp/ server.go:132 runServer()
- Step 01internal/
command/ mcp/ server.go:132 The setup workflow reads the logged-in user's Fly token and places it in the persistent proxy argument list.
go if token := getAccessToken(ctx); token != "" { args = append(args, "--bearer-token", token) } - Step 02internal/
command/ mcp/ server.go:145 The complete credential-bearing command is passed to the client-specific configuration updater.
go return mcpConfig.UpdateConfig(client, serverName, command, args) - Step 03internal/
command/ mcp/ config.go:570 Before the fix, normal creation and rename honored the ambient umask, so a newly serialized secret could remain world-readable.
go if err := os.WriteFile(tmp, contents, 0o666); err != nil { return err } return os.Rename(tmp, path) - Sinkinternal/
command/ mcp/ config.go:570 UpdateConfig()
Impact
Reported impact
A separate local user can steal the victim's long-lived Fly API token and exercise the token's cloud permissions, potentially reading application data, changing deployments, or deleting resources according to the victim's account authority.
Attack surface
Local multi-user Unix systems where flyctl mcp server configures a stream/SSE MCP client and persists a Fly bearer token.
Preconditions
The victim must run the setup command while logged in to Fly, and the generated client config must be in a directory readable by another local user. The attacker needs only an unprivileged local account.
Attack path
- 1
Wait for the victim to configure a supported MCP client through flyctl.
- 2
Read the resulting JSON configuration from the victim's home directory when its mode is
0644. - 3
Extract the
--bearer-tokenargument. - 4
Use the token with the same Fly API authority as the victim until it is rotated or expires.
Proof of concept
- 01
Environment setup
Use an isolated Unix host with two unprivileged accounts and a vulnerable flyctl build. Set the victim shell's umask to
022. - 02
Target configuration
Log the victim into a test Fly organization and configure a stream/SSE MCP client so flyctl persists
--bearer-tokenin its JSON settings. - 03
Exploit delivery
From the second local account, read the generated configuration and locate the bearer-token argument.
- 04
Expected response
On the vulnerable build, the file mode is
0644and the second account can read the complete token value. - 05
Outcome
PR #5192 forces the final file to
0600; the second account receives a permission error and cannot read the credential.
Remediation
Guidance
Upgrade to a flyctl release containing PR #5192 and rotate any token previously written to a permissive client config. Prefer an OS credential store or owner-only credential file over process arguments for long-lived secrets.
os.WriteFile(path, contents, 0o666)os.WriteFile(tmp, contents, 0o600)os.Chmod(tmp, 0o600)os.Rename(tmp, path)
Check the upstream record for the project's current remediation status.
Investigate the paths that matter in your codebase.
Winfunc can trace relevant code paths, preserve supporting evidence, and prepare remediation suggestions for engineering review within an agreed scope.
