Share the git URL check, and refuse the transports Omarchy does not clone from (backport of #8174)
Backport of the shared URL check (PR #8174, merged to quattro as 68ab12f7) onto
the v4-0-1 release branch, on top of the #8067 backport it follows.
omarchy-theme-install and omarchy-plugin-add both clone a URL a stranger can
choose, and each carried its own copy of the rule that refuses a git option or a
transport helper before cloning. The rule now lives in omarchy-git-url-check and
both callers ask it: two copies of a security check drift, and the second copy
arrived four months after the first only because someone went looking for it.
That rule was also enforcing half of what it described. <helper>::<address> is
one of two shapes git resolves a remote helper from -- it also runs
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 reach the same helper while
only the first was refused. Nothing exploitable follows on a stock system:
protocol.ext.allow defaults to never, and the :// spelling hands git-remote-ext
a command name it cannot exec. But a third-party helper installed on PATH is
reachable through the scheme form alone, and a guard is worth more when it
enforces the rule it states.
The :// shape cannot be refused the way :: is, because it is also how every
legitimate URL arrives, so the scheme is checked against the transports git
still connects itself: ssh git git+ssh ssh+git http https ftp ftps file.
git+ssh and ssh+git are on that list because they are spelled like a helper and
read as plain ssh; leaving them off would refuse a URL that clones today. ext
and fd are off it deliberately. A single colon is always scp-style ssh and a
bare path is always a path, so neither needs constraining.
The check fails closed: the callers read a non-zero status as a refusal, so a
missing omarchy-git-url-check refuses the URL rather than waving it through.
Clean cherry-pick on top of the #8067 backport: every file is byte-identical to
quattro, so merging v4-0-1 into quattro resolves without a conflict. test/shell
passes: 189 files, including the one this adds. test/cli passes, so the new
command's metadata is well-formed. Exercised the check here: https, scp-style,
ssh, git+ssh and scp-style IPv6 all pass, while ext:: ext:// fd:: fd:// and a
leading-dash form are refused, as is an uppercase EXT:// spelling.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DEMYa9UWtroz93DhMTtcV
This commit is contained in:
1 parent
c7af36d0aa
commit
e713ff3166
6 files changed
+177
-17
No files matched your search
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