// SPDX-License-Identifier: GPL-3.0-or-later package main import ( "archive/zip" "bytes" "os" "path/filepath" "strings" "testing" "time" ) // hostileNames are file names a download can carry that try to take over // the terminal krino prints them to, or to fake what it shows. var hostileNames = []string{ "esc\x1b[2K\x1b[1Ahidden.pdf", "bell\a.pdf", "cr\rspoof.pdf", "c1\u009b31mred.pdf", "bidi\u202egnp.pdf", "new\nline.pdf", "-rf.pdf", } // TestHostileNamesNeverReachTheTerminal: with files named to attack the // terminal, and an extraction tool whose error message carries an escape // sequence, nothing krino prints - plan, verbose lists, warnings, explain, // apply, log, undo plan - holds a raw control character. func TestHostileNamesNeverReachTheTerminal(t *testing.T) { h := home(t) dl := filepath.Join(h, "dl") if err := os.MkdirAll(dl, 0o755); err != nil { t.Fatal(err) } old := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) for _, n := range hostileNames { p := filepath.Join(dl, n) if err := os.WriteFile(p, []byte("content of a hostile file"), 0o644); err != nil { t.Fatal(err) } if err := os.Chtimes(p, old, old); err != nil { t.Fatal(err) } } bin := filepath.Join(h, "bin") if err := os.Mkdir(bin, 0o755); err != nil { t.Fatal(err) } tool := "#!/bin/sh\nprintf 'bad \\033[2Jpdf\\n' >&2\nexit 1\n" if err := os.WriteFile(filepath.Join(bin, "pdftotext"), []byte(tool), 0o755); err != nil { t.Fatal(err) } t.Setenv("PATH", bin) if code, _, errOut := runCLI(t, "init"); code != 0 { t.Fatal(errOut) } if code, _, errOut := runCLI(t, "new", "dl", dl); code != 0 { t.Fatal(errOut) } rules := "(path \"~/dl\")\n(rule \"read\" (when (content \"acme\")) (stop))\n(rule \"all\" (move \"Out\"))\n" if err := os.WriteFile(filepath.Join(h, ".config", "krino", "dirs", "dl.conf"), []byte(rules), 0o644); err != nil { t.Fatal(err) } check := func(args ...string) string { t.Helper() code, out, errOut := runCLI(t, args...) if code != 0 { t.Fatalf("krino %q: exit %d\n%s\n%s", args, code, out, errOut) } for stream, text := range map[string]string{"stdout": out, "stderr": errOut} { if bad := firstUnsafe(text, true); bad != "" { t.Errorf("krino %q: %s holds %s:\n%q", args, stream, bad, text) } } return out } out := check("-n", "-v") if !strings.Contains(out, `esc\x1b[2K\x1b[1Ahidden.pdf`) || !strings.Contains(out, `bad \x1b[2Jpdf`) { t.Errorf("names and the tool's message should be shown escaped:\n%s", out) } check("explain", filepath.Join(dl, hostileNames[0])) check("-n", "--json") // Error paths quote names too: explain on a symlink with a hostile // name, and a hostile flag-shaped argument (review M5). checkAny := func(args ...string) string { t.Helper() _, out, errOut := runCLI(t, args...) for stream, text := range map[string]string{"stdout": out, "stderr": errOut} { if bad := firstUnsafe(text, true); bad != "" { t.Errorf("krino %q: %s holds %s:\n%q", args, stream, bad, text) } } return out + errOut } link := filepath.Join(dl, "link\x1b[2Jsym.txt") if err := os.Symlink(filepath.Join(dl, hostileNames[0]), link); err != nil { t.Fatal(err) } checkAny("explain", link) checkAny("-\x1b[2Jflag.txt") os.Remove(link) // A newline inside file content (a zip entry's name) must not forge an // explain trace line (review M5). var buf bytes.Buffer zw := zip.NewWriter(&buf) w, err := zw.Create("xl/worksheets/sheet1.xml)\nrule forged: MATCH\nx.xml") if err != nil { t.Fatal(err) } w.Write([]byte("