mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-10-19 01:50:51 -05:00
fix(cli): refuse host filesystem access from remote TUI clients (#14677)
This commit is contained in:
1 parent
c10954ad7a
commit
e565d19c6c
5 files changed
+56
-1
No files matched your search
@@ -100,6 +100,9 @@ environment variable (the --host-key flag takes precedence).`,
|
||||
onyx-cli serve --host 0.0.0.0 --port 2222
|
||||
onyx-cli serve --idle-timeout 30m --max-session-timeout 2h`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
// Sessions here belong to remote clients; keep them off the host filesystem.
|
||||
tui.RemoteMode = true
|
||||
|
||||
serverCfg := config.Load()
|
||||
if serverCfg.ServerURL == "" {
|
||||
return exitcodes.New(exitcodes.NotConfigured, "server URL is not configured\n Run: onyx-cli chat to complete first-time setup")
|
||||
|
||||
@@ -267,6 +267,10 @@ func cmdSelectModel(m Model, idxStr string) (Model, tea.Cmd) {
|
||||
}
|
||||
|
||||
func cmdAttach(m Model, pathStr string) (Model, tea.Cmd) {
|
||||
if RemoteMode {
|
||||
m.viewport.addWarning("/attach is disabled over SSH: paths resolve on the server host, not yours.")
|
||||
return m, nil
|
||||
}
|
||||
if pathStr == "" {
|
||||
m.viewport.addWarning("Usage: /attach <file_path>")
|
||||
return m, nil
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package tui
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -279,6 +281,43 @@ func TestSelectAgentFromPickerUsesIDWhenNameMatches(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttachRefusedInRemoteMode(t *testing.T) {
|
||||
RemoteMode = true
|
||||
t.Cleanup(func() { RemoteMode = false })
|
||||
|
||||
m := NewModel(config.DefaultConfig(), nil)
|
||||
m, cmd := cmdAttach(m, "/etc/passwd")
|
||||
|
||||
if cmd != nil {
|
||||
t.Fatal("expected no upload command in remote mode")
|
||||
}
|
||||
if len(m.viewport.entries) == 0 {
|
||||
t.Fatal("expected a warning entry")
|
||||
}
|
||||
got := m.viewport.entries[len(m.viewport.entries)-1].content
|
||||
if !strings.Contains(got, "disabled over SSH") {
|
||||
t.Errorf("warning = %q, want a refusal mentioning SSH", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectFileDropIgnoredInRemoteMode(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "secret.txt")
|
||||
if err := os.WriteFile(path, []byte("secret"), 0o600); err != nil {
|
||||
t.Fatalf("write temp file: %v", err)
|
||||
}
|
||||
|
||||
if got := detectFileDrop(path); got != path {
|
||||
t.Fatalf("local detectFileDrop = %q, want %q", got, path)
|
||||
}
|
||||
|
||||
RemoteMode = true
|
||||
t.Cleanup(func() { RemoteMode = false })
|
||||
|
||||
if got := detectFileDrop(path); got != "" {
|
||||
t.Errorf("remote detectFileDrop = %q, want no match", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectModelInvalidIndex(t *testing.T) {
|
||||
m := NewModel(config.DefaultConfig(), nil)
|
||||
m, _ = cmdSelectModel(m, "5")
|
||||
|
||||
@@ -204,8 +204,12 @@ type fileDropMsg struct {
|
||||
path string
|
||||
}
|
||||
|
||||
// detectFileDrop checks if the text looks like a file path.
|
||||
// detectFileDrop checks if the text looks like a file path. It never matches
|
||||
// in remote mode, where the path would resolve on the server host.
|
||||
func detectFileDrop(text string) string {
|
||||
if RemoteMode {
|
||||
return ""
|
||||
}
|
||||
cleaned := strings.Trim(text, "'\"")
|
||||
if cleaned == "" {
|
||||
return ""
|
||||
|
||||
@@ -14,6 +14,11 @@ const (
|
||||
MaxAPIKeyRetries = 5
|
||||
)
|
||||
|
||||
// RemoteMode marks this process as serving the TUI to remote clients.
|
||||
// Commands that touch the host filesystem are refused while it is set,
|
||||
// because those paths resolve on the operator's machine, not the client's.
|
||||
var RemoteMode bool
|
||||
|
||||
// --- auth prompt (bubbletea model) ---
|
||||
|
||||
type authState int
|
||||
|
||||
Reference in new issue
Block a user