diff options
| author | Craig Jennings <c@cjennings.net> | 2026-05-23 19:21:31 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-05-23 19:21:31 -0500 |
| commit | 64f349326454583a375a4f8d6df44966833e3512 (patch) | |
| tree | 93c69d404a96ee3122fe275c33bf43e29ae1961b /modules | |
| parent | b3dba452b13cddc51477f3bdcfef663783d3fa5b (diff) | |
| download | dotemacs-64f349326454583a375a4f8d6df44966833e3512.tar.gz dotemacs-64f349326454583a375a4f8d6df44966833e3512.zip | |
fix(dwim-shell): quote and validate user-controlled shell inputs
Several dwim-shell commands interpolated user-controlled strings straight into shell templates, so a value with spaces, quotes, or shell metacharacters could break out of the command. The worst was git-clone-clipboard-url, which dropped raw clipboard contents into "git clone <<cb>>".
I added three pure validators (git URL, ffmpeg timestamp, rename prefix) and fixed the interpolation sites. git-clone now validates the clipboard and passes the URL through shell-quote-argument instead of <<cb>>. The GPG recipient and the 7z archive name go through shell-quote-argument instead of hand-written single quotes. The thumbnail timestamp and the rename prefix are validated to a safe shape before they reach the command, so the unquoted interpolation that remains is constrained to digits, colons, and filename-safe characters.
The fifth case in the ticket, the video-concat filelist built with echo/tr/sed, is a redesign rather than a quoting fix and is filed as a follow-up.
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/dwim-shell-config.el | 57 |
1 files changed, 49 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 () |
