Skip to content

Commit 6676800

Browse files
AaronDDMclaude
andcommitted
fix(dashboard): never print client secret to stdout
Client secret was printed via fmt.Printf on app creation, leaking it to terminal scrollback, CI logs, and screen captures. Apply the same secure delivery pattern used for API keys: clipboard or temp file with 0600 permissions. The secret is never written to stdout. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d4faf3c commit 6676800

File tree

2 files changed

+44
-2
lines changed

2 files changed

+44
-2
lines changed

internal/cli/dashboard/apps.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,12 @@ func newAppsCreateCmd() *cobra.Command {
141141
fmt.Printf(" Environment: %s\n", app.Environment)
142142
}
143143

144-
_, _ = common.Yellow.Println("\n Client Secret (shown once — save it now):")
145-
fmt.Printf(" %s\n", app.ClientSecret)
144+
if app.ClientSecret != "" {
145+
_, _ = common.Yellow.Println("\n Client Secret (available once — save it now):")
146+
if err := handleSecretDelivery(app.ClientSecret, "Client Secret"); err != nil {
147+
return err
148+
}
149+
}
146150

147151
fmt.Println("\nTo configure the CLI with this application:")
148152
fmt.Printf(" nylas auth config --api-key <your-api-key> --region %s\n", app.Region)

internal/cli/dashboard/keys.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,44 @@ func handleAPIKeyDelivery(apiKey, appID, region string) error {
226226
return nil
227227
}
228228

229+
// handleSecretDelivery prompts the user to choose how to receive a secret.
230+
// Secrets are never printed to stdout to prevent leaking in terminal history or logs.
231+
func handleSecretDelivery(secret, label string) error {
232+
fmt.Printf("\nHow would you like to receive the %s?\n", label)
233+
fmt.Println()
234+
_, _ = common.Cyan.Println(" [1] Copy to clipboard (recommended)")
235+
fmt.Println(" [2] Save to file")
236+
fmt.Println()
237+
238+
choice, err := readLine("Choose [1-2]: ")
239+
if err != nil {
240+
return wrapDashboardError(err)
241+
}
242+
243+
switch choice {
244+
case "1", "":
245+
if err := common.CopyToClipboard(secret); err != nil {
246+
_, _ = common.Yellow.Printf(" Clipboard unavailable: %v\n", err)
247+
_, _ = common.Dim.Println(" Try option [2] to save to a file instead")
248+
return nil
249+
}
250+
_, _ = common.Green.Printf("✓ %s copied to clipboard\n", label)
251+
252+
case "2":
253+
keyFile := filepath.Join(os.TempDir(), "nylas-client-secret.txt")
254+
if err := os.WriteFile(keyFile, []byte(secret+"\n"), 0o600); err != nil { // #nosec G306
255+
return wrapDashboardError(fmt.Errorf("failed to write file: %w", err))
256+
}
257+
_, _ = common.Green.Printf("✓ %s saved to: %s\n", label, keyFile)
258+
_, _ = common.Dim.Println(" Read it, then delete the file")
259+
260+
default:
261+
return dashboardError("invalid selection", "Choose 1-2")
262+
}
263+
264+
return nil
265+
}
266+
229267
// activateAPIKey stores the API key and configures the CLI to use it.
230268
func activateAPIKey(apiKey, clientID, region string) error {
231269
configStore := config.NewDefaultFileStore()

0 commit comments

Comments
 (0)