aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--modules/dwim-shell-config.el57
-rw-r--r--tests/test-dwim-shell-config-input-safety.el71
2 files changed, 120 insertions, 8 deletions
diff --git a/modules/dwim-shell-config.el b/modules/dwim-shell-config.el
index febfa709..41b9231f 100644
--- a/modules/dwim-shell-config.el
+++ b/modules/dwim-shell-config.el
@@ -137,6 +137,38 @@ starts, the temp file is cleaned up synchronously instead."
(when (file-exists-p temp-file)
(delete-file temp-file))))))
+;; ---------------------------- Input safety helpers ---------------------------
+
+(defun cj/dwim-shell--valid-git-url-p (url)
+ "Return non-nil when URL looks like a safe git clone URL.
+Accepts http(s)://, git://, ssh://, and scp-style host:path forms, and rejects
+empty strings, whitespace, and shell metacharacters. The clone site also
+quotes the URL with `shell-quote-argument'; this check is the first line of
+defense and keeps junk clipboard text from being cloned."
+ (and (stringp url)
+ (not (string-empty-p url))
+ (string-match-p
+ (concat "\\`\\(?:https?://\\|git://\\|ssh://\\|"
+ "[A-Za-z0-9._-]+@[A-Za-z0-9._-]+:\\)"
+ "[A-Za-z0-9._:/~@-]+\\'")
+ url)))
+
+(defun cj/dwim-shell--valid-ffmpeg-timestamp-p (timestamp)
+ "Return non-nil when TIMESTAMP is a plain-seconds or HH:MM:SS ffmpeg time.
+Accepts forms like \"5\", \"90.5\", \"00:05\", and \"1:02:03.5\"; rejects
+negatives, non-numeric text, and anything carrying shell metacharacters."
+ (and (stringp timestamp)
+ (string-match-p "\\`\\(?:[0-9]+:\\)\\{0,2\\}[0-9]+\\(?:\\.[0-9]+\\)?\\'"
+ timestamp)))
+
+(defun cj/dwim-shell--safe-rename-prefix-p (prefix)
+ "Return non-nil when PREFIX is a filename-safe rename prefix.
+Allows alphanumerics, spaces, dot, dash, and underscore (empty is fine), and
+rejects quotes, slashes, and shell metacharacters that would break out of the
+single-quoted destination it is interpolated into."
+ (and (stringp prefix)
+ (string-match-p "\\`[[:alnum:] ._-]*\\'" prefix)))
+
;; ----------------------------- Dwim Shell Command ----------------------------
(use-package dwim-shell-command
@@ -432,12 +464,17 @@ process list, and the file is removed only after the spawned process exits."
(defun cj/dwim-shell-commands-git-clone-clipboard-url ()
- "Clone git URL in clipboard to `default-directory'."
+ "Clone the git URL in the clipboard to `default-directory'.
+Validates the clipboard as a git URL and passes it as a quoted argument, so
+clipboard contents cannot inject shell commands."
(interactive)
- (dwim-shell-command-on-marked-files
- (format "Clone %s" (file-name-base (current-kill 0)))
- "git clone <<cb>>"
- :utils "git"))
+ (let ((url (string-trim (current-kill 0))))
+ (unless (cj/dwim-shell--valid-git-url-p url)
+ (user-error "Clipboard does not contain a valid git URL: %s" url))
+ (dwim-shell-command-on-marked-files
+ (format "Clone %s" (file-name-base url))
+ (format "git clone %s" (shell-quote-argument url))
+ :utils "git")))
(defun cj/dwim-shell-commands-open-file-manager ()
"Open the default file manager in the current directory."
@@ -552,6 +589,8 @@ process list, and the file is removed only after the spawned process exits."
"Extract thumbnail from video at specific time."
(interactive)
(let ((time (read-string "Time (HH:MM:SS or seconds): " "00:00:05")))
+ (unless (cj/dwim-shell--valid-ffmpeg-timestamp-p time)
+ (user-error "Not a valid timestamp (use seconds or HH:MM:SS): %s" time))
(dwim-shell-command-on-marked-files
"Extract video thumbnail"
(format "ffmpeg -i '<<f>>' -ss %s -vframes 1 '<<fne>>_thumb.jpg'" time)
@@ -678,9 +717,9 @@ in the process list."
password
"Create encrypted archive"
(lambda (temp-file)
- (format "7z a -t7z -mhe=on -p\"$(cat '%s')\" '%s.7z' '<<*>>'"
+ (format "7z a -t7z -mhe=on -p\"$(cat '%s')\" %s '<<*>>'"
temp-file
- archive-name))
+ (shell-quote-argument (concat archive-name ".7z"))))
:utils "7z")))
@@ -722,6 +761,8 @@ in the process list."
"Rename files with sequential numbers."
(interactive)
(let ((prefix (read-string "Prefix (optional): ")))
+ (unless (cj/dwim-shell--safe-rename-prefix-p prefix)
+ (user-error "Prefix may only contain letters, numbers, space, . _ -: %s" prefix))
(dwim-shell-command-on-marked-files
"Number files"
(format "mv '<<f>>' '<<d>>/%s<<n>>.<<e>>'" prefix)
@@ -743,7 +784,7 @@ in the process list."
"GPG encrypt"
(if (string-empty-p recipient)
"gpg --symmetric --cipher-algo AES256 '<<f>>'"
- (format "gpg --encrypt --recipient '%s' '<<f>>'" recipient))
+ (format "gpg --encrypt --recipient %s '<<f>>'" (shell-quote-argument recipient)))
:utils "gpg")))
(defun cj/dwim-shell-commands-decrypt-with-gpg ()
diff --git a/tests/test-dwim-shell-config-input-safety.el b/tests/test-dwim-shell-config-input-safety.el
new file mode 100644
index 00000000..2ff2edc9
--- /dev/null
+++ b/tests/test-dwim-shell-config-input-safety.el
@@ -0,0 +1,71 @@
+;;; test-dwim-shell-config-input-safety.el --- Tests for input validators -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Covers the pure input validators that guard user-controlled strings before
+;; they reach a shell command: git clone URLs, ffmpeg timestamps, and rename
+;; prefixes. These reject shell metacharacters and malformed input so the
+;; command-construction sites stay injection-safe.
+
+;;; Code:
+
+(when noninteractive
+ (package-initialize))
+
+(require 'ert)
+(require 'dwim-shell-config)
+
+;; ---------------------------------------------------------------------------
+;;; cj/dwim-shell--valid-git-url-p
+;; ---------------------------------------------------------------------------
+
+(ert-deftest test-dwim-valid-git-url-accepts-common-forms ()
+ "Normal: https, scp-style, ssh, and git URLs are accepted."
+ (dolist (url '("https://github.com/user/repo.git"
+ "http://example.com/x"
+ "git@github.com:user/repo.git"
+ "ssh://git@host/path/repo.git"
+ "git://host/path"))
+ (should (cj/dwim-shell--valid-git-url-p url))))
+
+(ert-deftest test-dwim-valid-git-url-rejects-empty-and-junk ()
+ "Boundary: empty string and non-URL text are rejected."
+ (should-not (cj/dwim-shell--valid-git-url-p ""))
+ (should-not (cj/dwim-shell--valid-git-url-p "not a url"))
+ (should-not (cj/dwim-shell--valid-git-url-p " ")))
+
+(ert-deftest test-dwim-valid-git-url-rejects-shell-metacharacters ()
+ "Error: URLs carrying shell metacharacters are rejected."
+ (should-not (cj/dwim-shell--valid-git-url-p "https://x.com;reboot"))
+ (should-not (cj/dwim-shell--valid-git-url-p "https://x.com; rm -rf /"))
+ (should-not (cj/dwim-shell--valid-git-url-p "https://x.com$(whoami)")))
+
+;; ---------------------------------------------------------------------------
+;;; cj/dwim-shell--valid-ffmpeg-timestamp-p
+;; ---------------------------------------------------------------------------
+
+(ert-deftest test-dwim-valid-timestamp-accepts-seconds-and-clock ()
+ "Normal: plain seconds and HH:MM:SS forms are accepted."
+ (dolist (ts '("5" "0" "90.5" "00:05" "00:00:05" "1:02:03.5"))
+ (should (cj/dwim-shell--valid-ffmpeg-timestamp-p ts))))
+
+(ert-deftest test-dwim-valid-timestamp-rejects-bad-input ()
+ "Error: non-numeric, negative, or metacharacter-bearing timestamps rejected."
+ (dolist (ts '("" "abc" "-5" "5; rm" "00:00:05; reboot"))
+ (should-not (cj/dwim-shell--valid-ffmpeg-timestamp-p ts))))
+
+;; ---------------------------------------------------------------------------
+;;; cj/dwim-shell--safe-rename-prefix-p
+;; ---------------------------------------------------------------------------
+
+(ert-deftest test-dwim-safe-rename-prefix-accepts-filename-safe ()
+ "Normal/Boundary: alphanumerics, spaces, dot, dash, underscore, and empty."
+ (dolist (p '("" "img_" "vacation 2026" "shot-01."))
+ (should (cj/dwim-shell--safe-rename-prefix-p p))))
+
+(ert-deftest test-dwim-safe-rename-prefix-rejects-unsafe ()
+ "Error: quotes, slashes, semicolons, and newlines are rejected."
+ (dolist (p '("a'b" "a/b" "a;b" "a\nb"))
+ (should-not (cj/dwim-shell--safe-rename-prefix-p p))))
+
+(provide 'test-dwim-shell-config-input-safety)
+;;; test-dwim-shell-config-input-safety.el ends here