aboutsummaryrefslogtreecommitdiff
path: root/cmd/krino/hostile_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/krino/hostile_test.go')
-rw-r--r--cmd/krino/hostile_test.go86
1 files changed, 86 insertions, 0 deletions
diff --git a/cmd/krino/hostile_test.go b/cmd/krino/hostile_test.go
new file mode 100644
index 0000000..a2b5d39
--- /dev/null
+++ b/cmd/krino/hostile_test.go
@@ -0,0 +1,86 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package main
+
+import (
+ "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("-y")
+ check("log")
+ check("undo", "-n")
+}