Share the git URL check, and refuse the transports Omarchy does not clone from (#8174)
* Share the git URL check between theme-install and plugin-add Both commands clone a URL a stranger can choose, and each carried its own copy of the rule that refuses a git option or a `<helper>::<address>` transport helper before cloning. Two copies of a security check drift: the second one arrived four months after the first, and only because someone went looking for it. The rule now lives in omarchy-git-url-check and the callers ask it. Its absence refuses the URL rather than waving it through, since the callers read a non-zero status as a refusal and a missing command exits 127. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-Authored-By: Codex XHigh <noreply@openai.com> * Refuse a git URL naming a transport Omarchy does not clone from `<helper>::<address>` is only one of the two ways a URL reaches a remote helper. git also resolves git-remote-<scheme> for `<scheme>://<address>` whenever the scheme is not one it connects itself, so `ext::sh -c id` and `ext://sh -c id` arrive at the same helper while only the first was refused. That shape cannot be refused outright, because it is also how every legitimate URL arrives, so the scheme is checked against the transports git still connects itself. `git+ssh` and `ssh+git` are on that list: they are spelled like a helper and read as plain ssh, and leaving them off would refuse a URL that clones today. `ext` and `fd` are off it deliberately -- git ships a helper for each, and `ext` runs whatever command the URL carries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-Authored-By: Codex XHigh <noreply@openai.com> --------- Co-authored-by: David Heinemeier Hansson <david@hey.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Codex XHigh <noreply@openai.com>
This commit is contained in:
co-authored by
Claude Opus 5
Codex XHigh
David Heinemeier Hansson
parent
30471bf35a
commit
68ab12f77d
Executable
+50
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Check that a git URL names a repository, not a transport helper
|
||||
# omarchy:args=<git-url>
|
||||
# omarchy:hidden=true
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# git picks a remote helper -- an executable it runs at clone time -- out of a URL
|
||||
# in exactly two shapes, and no others: `<helper>::<address>`, and
|
||||
# `<scheme>://<address>` for any scheme git does not handle itself. A single
|
||||
# colon is always scp-style ssh, and a bare path is always a path; neither can
|
||||
# reach a helper. So constraining those two shapes covers the whole surface.
|
||||
#
|
||||
# The `::` shape is refused outright, because no helper reachable that way is one
|
||||
# a theme or plugin URL has business naming, and `ext::` runs a shell command.
|
||||
# The `://` shape cannot be refused the same way, since it is also how every
|
||||
# legitimate URL arrives -- so it is allowlisted instead. The list is the
|
||||
# transports git still connects itself, `git+ssh` and `ssh+git` included: those
|
||||
# two are spelled like a helper but are read as plain ssh. `ext` and `fd` are
|
||||
# left out deliberately -- git ships a helper for each, and `ext` runs whatever
|
||||
# command the URL carries.
|
||||
TRANSPORTS=(ssh git git+ssh ssh+git http https ftp ftps file)
|
||||
|
||||
fail() {
|
||||
echo "omarchy-git-url-check: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
url="${1-}"
|
||||
|
||||
if [[ -z $url ]]; then
|
||||
fail "a git URL is required"
|
||||
fi
|
||||
|
||||
if [[ $url == -* || $url =~ ^[A-Za-z0-9][A-Za-z0-9+.-]*:: ]]; then
|
||||
fail "'$url' names a git option or transport helper, not a repository."
|
||||
fi
|
||||
|
||||
if [[ $url =~ ^([A-Za-z0-9][A-Za-z0-9+.-]*):// ]]; then
|
||||
scheme="${BASH_REMATCH[1]}"
|
||||
|
||||
for transport in "${TRANSPORTS[@]}"; do
|
||||
if [[ $scheme == "$transport" ]]; then
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
|
||||
fail "'$url' names the '$scheme' transport, which Omarchy does not clone from."
|
||||
fi
|
||||
@@ -93,14 +93,12 @@ if [[ -z $url ]]; then
|
||||
[[ -n $url ]] || fail "a git URL is required"
|
||||
fi
|
||||
|
||||
# git reads a leading dash as an option, and `<helper>::<address>` as a remote
|
||||
# helper to run at clone time. Reject both so an untrusted URL cannot smuggle a
|
||||
# transport helper that executes before the plugin is validated or enabled. An
|
||||
# scp-style IPv6 host such as git@[2001:db8::1]:org/repo.git carries `::` too and
|
||||
# must still clone.
|
||||
if [[ $url == -* || $url =~ ^[A-Za-z0-9][A-Za-z0-9+.-]*:: ]]; then
|
||||
fail "'$url' names a git option or transport helper, not a repository."
|
||||
fi
|
||||
# Refuse a URL that names a git option or a transport helper before cloning, so
|
||||
# an untrusted URL cannot run a command before the plugin is validated or
|
||||
# enabled. The check is shared with omarchy-theme-install and explains itself; a
|
||||
# missing checker leaves this non-zero, which refuses the URL rather than
|
||||
# cloning it.
|
||||
omarchy-git-url-check "$url" || exit 1
|
||||
|
||||
if (( ! ASSUME_YES )); then
|
||||
cat >&2 <<WARN
|
||||
|
||||
@@ -16,14 +16,10 @@ if [[ -z $REPO_URL ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# git reads a leading dash as an option, and `<helper>::<address>` as a remote
|
||||
# helper to run. The helper name is a bare word at the very start, which is what
|
||||
# this matches; an scp-style IPv6 host such as git@[2001:db8::1]:org/repo.git
|
||||
# carries `::` too and must still clone.
|
||||
if [[ $REPO_URL == -* || $REPO_URL =~ ^[A-Za-z0-9][A-Za-z0-9+.-]*:: ]]; then
|
||||
echo "Error: '$REPO_URL' names a git option or transport helper, not a repository."
|
||||
exit 1
|
||||
fi
|
||||
# Refuse a URL that names a git option or a transport helper before cloning. The
|
||||
# check is shared with omarchy-plugin-add and explains itself; a missing checker
|
||||
# leaves this non-zero, which refuses the URL rather than cloning it.
|
||||
omarchy-git-url-check "$REPO_URL" || exit 1
|
||||
|
||||
THEMES_DIR="$HOME/.config/omarchy/themes"
|
||||
|
||||
|
||||
Executable
+79
@@ -0,0 +1,79 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# omarchy-git-url-check decides which URLs omarchy-theme-install and
|
||||
# omarchy-plugin-add are willing to hand to `git clone`. git resolves a remote
|
||||
# helper -- a program it runs at clone time -- from exactly two URL shapes,
|
||||
# `<helper>::<address>` and `<scheme>://<address>`, so those are the two shapes
|
||||
# asserted here, alongside every legitimate form a user is likely to paste.
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
check() {
|
||||
"$ROOT/bin/omarchy-git-url-check" "$@" 2>&1
|
||||
}
|
||||
|
||||
# `<helper>::<address>`, the shape that runs a program. `ext::` is the dangerous
|
||||
# one: git runs the rest as a shell command once protocol.ext.allow permits it.
|
||||
for url in "ext::sh -c id" "fd::0,1" "gcrypt::x" "a+b::x" "a.b::x" "a-b::x" "1::x"; do
|
||||
output=$(check "$url") &&
|
||||
fail "omarchy-git-url-check refuses the transport helper '$url'" "$output"
|
||||
grep -qF "names a git option or transport helper" <<<"$output" ||
|
||||
fail "omarchy-git-url-check names the helper rejection for '$url'" "$output"
|
||||
done
|
||||
|
||||
pass "a <helper>::<address> URL is refused"
|
||||
|
||||
# `<scheme>://<address>`, the shape #8067 left open: git looks up
|
||||
# git-remote-<scheme> for any scheme it does not implement itself, so an
|
||||
# allowlist is the only form of this check that holds.
|
||||
for url in "ext://sh -c id" "fd://17" "gcrypt://example.com/x" "zzz://a" "ZZZ://a" "HTTPS://github.com/a/b"; do
|
||||
output=$(check "$url") &&
|
||||
fail "omarchy-git-url-check refuses the '$url' transport" "$output"
|
||||
grep -qF "which Omarchy does not clone from" <<<"$output" ||
|
||||
fail "omarchy-git-url-check names the transport rejection for '$url'" "$output"
|
||||
done
|
||||
|
||||
pass "a <scheme>://<address> URL outside git's own transports is refused"
|
||||
|
||||
# A leading dash is an option to git, not a URL.
|
||||
for url in "-x" "--upload-pack=touch /tmp/pwned" "-oProxyCommand=x"; do
|
||||
output=$(check "$url") &&
|
||||
fail "omarchy-git-url-check refuses the option '$url'" "$output"
|
||||
done
|
||||
|
||||
pass "a URL shaped like a git option is refused"
|
||||
|
||||
output=$(check "") && fail "omarchy-git-url-check refuses an empty URL" "$output"
|
||||
output=$(check) && fail "omarchy-git-url-check refuses a missing URL" "$output"
|
||||
|
||||
pass "an empty URL is refused"
|
||||
|
||||
# Everything a user actually pastes. The scp-style forms carry a single colon,
|
||||
# which git never reads as a helper, and the IPv6 host carries `::` inside
|
||||
# brackets rather than at the start.
|
||||
for url in \
|
||||
"https://github.com/acme/omarchy-weather.git" \
|
||||
"http://example.com/a/b.git" \
|
||||
"https://user:token@github.com/acme/repo.git" \
|
||||
"ssh://git@github.com/acme/repo.git" \
|
||||
"ssh://git@[2001:db8::1]:22/org/repo.git" \
|
||||
"git://example.com/repo.git" \
|
||||
"git+ssh://git@example.com/acme/repo.git" \
|
||||
"ssh+git://git@example.com/acme/repo.git" \
|
||||
"ftp://example.com/repo.git" \
|
||||
"ftps://example.com/repo.git" \
|
||||
"file:///home/me/repo" \
|
||||
"git@github.com:acme/repo.git" \
|
||||
"git@[2001:db8::1]:org/repo.git" \
|
||||
"host:-s/foo.git" \
|
||||
"/home/me/repo" \
|
||||
"./repo" \
|
||||
"../repo" \
|
||||
"repo"; do
|
||||
output=$(check "$url") ||
|
||||
fail "omarchy-git-url-check accepts the legitimate URL '$url'" "$output"
|
||||
done
|
||||
|
||||
pass "the URL forms a user pastes are accepted"
|
||||
@@ -111,6 +111,20 @@ for bad in "ext::sh -c touch /tmp/omarchy-guard-test" "fd::17"; do
|
||||
done
|
||||
pass "plugin add rejects transport-helper URLs before cloning"
|
||||
|
||||
# The `://` spelling of the same thing: git resolves git-remote-<scheme> for any
|
||||
# scheme it does not implement itself, so `ext::` and `ext://` reach the same
|
||||
# helper and both have to be refused.
|
||||
for bad in "ext://sh -c id" "gcrypt://example.com/x"; do
|
||||
rm -f "$clone_marker"
|
||||
output=$(add_url "$bad") &&
|
||||
fail "plugin add rejects a transport-scheme URL: $bad" "$output"
|
||||
grep -qF "which Omarchy does not clone from" <<<"$output" ||
|
||||
fail "plugin add names the transport-scheme rejection: $bad" "$output"
|
||||
[[ ! -e $clone_marker ]] ||
|
||||
fail "plugin add reached git clone for a transport-scheme URL: $bad"
|
||||
done
|
||||
pass "plugin add rejects transport-scheme URLs before cloning"
|
||||
|
||||
# Option-shaped URLs on argv are refused before clone — by the option parser
|
||||
# (`-*` falls to "unknown add option"), not the guard. The guard's own
|
||||
# leading-dash arm is only reachable through the interactive gum prompt and is
|
||||
|
||||
@@ -40,7 +40,7 @@ install_theme() {
|
||||
: >"$git_calls"
|
||||
: >"$theme_calls"
|
||||
|
||||
HOME="$test_tmp/home" PATH="$mock_bin:$PATH" \
|
||||
HOME="$test_tmp/home" PATH="${2-$mock_bin:$ROOT/bin:$PATH}" \
|
||||
OMARCHY_TEST_GIT_CALLS="$git_calls" OMARCHY_TEST_THEME_CALLS="$theme_calls" \
|
||||
bash "$ROOT/bin/omarchy-theme-install" "$1" >"$test_tmp/out" 2>&1 || return $?
|
||||
}
|
||||
@@ -58,6 +58,29 @@ done
|
||||
|
||||
pass "a URL that names a git option or a transport helper never reaches git"
|
||||
|
||||
# git resolves git-remote-<scheme> for any scheme it does not implement itself,
|
||||
# so the `://` spelling of a helper has to be refused as well as the `::` one.
|
||||
for url in "ext://sh -c id" "fd://17" "gcrypt://example.com/x"; do
|
||||
if install_theme "$url"; then
|
||||
fail "omarchy-theme-install refuses the URL '$url'"
|
||||
fi
|
||||
|
||||
[[ ! -s $git_calls ]] || fail "omarchy-theme-install refuses '$url' before running git" "$(cat "$git_calls")"
|
||||
done
|
||||
|
||||
pass "a URL naming a transport git does not implement never reaches git"
|
||||
|
||||
# The checker is a separate command, so its absence has to refuse the URL rather
|
||||
# than wave it through to git.
|
||||
if install_theme "https://github.com/example/omarchy-cool-theme.git" "$mock_bin:$PATH"; then
|
||||
fail "omarchy-theme-install refuses a URL it cannot check"
|
||||
fi
|
||||
|
||||
[[ ! -s $git_calls ]] ||
|
||||
fail "omarchy-theme-install refuses an unchecked URL before running git" "$(cat "$git_calls")"
|
||||
|
||||
pass "a missing url checker refuses the URL instead of cloning it"
|
||||
|
||||
# A URL whose derived name would escape the themes directory.
|
||||
for url in "https://example.com/..git" "https://example.com/.git"; do
|
||||
if install_theme "$url"; then
|
||||
|
||||
Reference in New Issue
Block a user