Merge pull request #8496 from Chessing234/security/webapp-http-only

This commit is contained in:
Ryan Hughes
2026-08-29 03:32:08 -04:00
2 changed files with 156 additions and 7 deletions
+32 -7
View File
@@ -54,6 +54,34 @@ download_icon() {
[[ -s $2 && $(file -b --mime-type "$2") == image/* ]]
}
# Chromium --app= treats javascript:, file:, and data: as a document to
# run. Prefix schemeless input with https as before, then refuse anything
# that is not http(s).
normalize_webapp_url() {
local url=$1
if [[ ! $url =~ ^[a-zA-Z][a-zA-Z0-9+.-]*: ]]; then
url="https://$url"
fi
printf '%s' "$url"
}
# Raw whitespace must be percent-encoded in a URL. Refuse it before serializing
# the desktop entry; before Exec argument quoting, it also split browser flags
# and additional URLs into separate arguments. Schemes are case-insensitive.
require_http_url() {
local url=$1
if [[ $url =~ [[:space:]] ]]; then
echo "Error: web app URL must not contain whitespace." >&2
exit 1
fi
if [[ ! ${url,,} =~ ^https?:// ]]; then
echo "Error: web app URL must be http or https." >&2
exit 1
fi
}
fetch_site_icon() {
local site_url="$1" dest="$2"
local origin page icon_url
@@ -113,9 +141,8 @@ if (( $# < 3 )); then
APP_NAME=$(gum input --prompt "Name> " --placeholder "My favorite web app")
require_plain_name "$APP_NAME"
APP_URL=$(gum input --prompt "URL> " --placeholder "https://example.com")
if [[ ! $APP_URL =~ ^[a-zA-Z][a-zA-Z0-9+.-]*: ]]; then
APP_URL="https://$APP_URL"
fi
APP_URL=$(normalize_webapp_url "$APP_URL")
require_http_url "$APP_URL"
# Try to fetch the site's icon automatically first.
mkdir -p "$ICON_DIR"
@@ -132,10 +159,8 @@ if (( $# < 3 )); then
INTERACTIVE_MODE=true
else
APP_NAME="$1"
APP_URL="$2"
if [[ ! $APP_URL =~ ^[a-zA-Z][a-zA-Z0-9+.-]*: ]]; then
APP_URL="https://$APP_URL"
fi
APP_URL=$(normalize_webapp_url "$2")
require_http_url "$APP_URL"
ICON_REF="$3"
CUSTOM_EXEC="$4" # Optional custom exec command
MIME_TYPES="$5" # Optional mime types
+124
View File
@@ -0,0 +1,124 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
home="$tmpdir/home"
mkdir -p "$home/.local/share/applications"
install_webapp() {
HOME="$home" "$ROOT/bin/omarchy-webapp-install" "$@"
}
desktop_for() {
printf '%s' "$home/.local/share/applications/$1.desktop"
}
if install_webapp "Example" "https://example.com" "webapp" >"$tmpdir/out" 2>"$tmpdir/err"; then
:
else
fail "webapp install accepts an https URL" "$(cat "$tmpdir/err")"
fi
desktop=$(desktop_for Example)
[[ -f $desktop ]] || fail "webapp install writes a desktop file"
grep -Fxq 'Name=Example' "$desktop" || fail "webapp install writes the app name"
grep -Fxq 'Exec=omarchy-launch-webapp "https://example.com"' "$desktop" ||
fail "webapp install launches the https URL" "$(cat "$desktop")"
pass "webapp install writes an https desktop entry"
if install_webapp "Plain" "example.org/app" "webapp" >"$tmpdir/out" 2>"$tmpdir/err"; then
:
else
fail "webapp install prefixes a schemeless URL with https" "$(cat "$tmpdir/err")"
fi
grep -Fxq 'Exec=omarchy-launch-webapp "https://example.org/app"' "$(desktop_for Plain)" ||
fail "webapp install stores the prefixed https URL" "$(cat "$(desktop_for Plain)")"
pass "webapp install prefixes a schemeless URL with https"
if install_webapp "Local" "https://localhost:47990" "webapp" "omarchy-launch-webapp https://localhost:47990 --ignore-certificate-errors" >"$tmpdir/out" 2>"$tmpdir/err"; then
:
else
fail "webapp install keeps a custom https exec" "$(cat "$tmpdir/err")"
fi
grep -Fxq 'Exec=omarchy-launch-webapp https://localhost:47990 --ignore-certificate-errors' "$(desktop_for Local)" ||
fail "webapp install writes the custom exec" "$(cat "$(desktop_for Local)")"
pass "webapp install keeps a custom https exec"
for url in "javascript:alert(1)" "file:///etc/passwd" "data:text/html,hi" "ftp://example.com" "ext://x"; do
if install_webapp "Bad" "$url" "webapp" >"$tmpdir/out" 2>"$tmpdir/err"; then
fail "webapp install refuses '$url'"
fi
grep -Fq 'must be http or https' "$tmpdir/err" ||
fail "webapp install names the scheme refusal for '$url'" "$(cat "$tmpdir/err")"
[[ ! -e $(desktop_for Bad) ]] || fail "webapp install does not write a desktop file for '$url'"
done
pass "webapp install refuses non-http(s) URLs"
# Raw whitespace is not valid URL data, and before Exec argument quoting it
# split browser flags or additional URLs into separate arguments.
for url in \
" javascript:alert(1)" \
" file:///etc/passwd" \
"https://example.com data:text/html,hi" \
"https://example.com/ --user-agent=INJECTION_PROOF_MARKER_12345"; do
if install_webapp "Sneak" "$url" "webapp" >"$tmpdir/out" 2>"$tmpdir/err"; then
fail "webapp install refuses whitespace in '$url'" "$(cat "$(desktop_for Sneak)")"
fi
grep -Fq 'must not contain whitespace' "$tmpdir/err" ||
fail "webapp install names the whitespace refusal for '$url'" "$(cat "$tmpdir/err")"
[[ ! -e $(desktop_for Sneak) ]] || fail "webapp install writes no desktop file for '$url'"
done
pass "webapp install refuses a URL carrying whitespace"
# Schemes are case-insensitive, and HTTPS://example.com installed before the
# scheme test existed.
if install_webapp "Upper" "HTTPS://example.com" "webapp" >"$tmpdir/out" 2>"$tmpdir/err"; then
:
else
fail "webapp install accepts an uppercase scheme" "$(cat "$tmpdir/err")"
fi
grep -Fxq 'Exec=omarchy-launch-webapp "HTTPS://example.com"' "$(desktop_for Upper)" ||
fail "webapp install keeps the uppercase scheme" "$(cat "$(desktop_for Upper)")"
pass "webapp install accepts an uppercase http scheme"
# The interactive prompt fetches the site's icon, so a refused URL must be
# refused before anything dereferences it.
stubs="$tmpdir/stubs"
mkdir -p "$stubs"
cat >"$stubs/gum" <<'GUM'
#!/bin/bash
count=$(cat "$GUM_COUNT" 2>/dev/null || echo 0)
count=$((count + 1))
printf '%s\n' "$count" >"$GUM_COUNT"
sed -n "${count}p" "$GUM_ANSWERS"
GUM
cat >"$stubs/curl" <<'CURL'
#!/bin/bash
printf '%s\n' "$*" >>"$CURL_LOG"
exit 1
CURL
chmod +x "$stubs/gum" "$stubs/curl"
printf 'Evil\nfile:///etc/passwd\n' >"$tmpdir/answers"
: >"$tmpdir/gum-count"
: >"$tmpdir/curl-log"
if GUM_ANSWERS="$tmpdir/answers" GUM_COUNT="$tmpdir/gum-count" CURL_LOG="$tmpdir/curl-log" \
PATH="$stubs:$PATH" HOME="$home" "$ROOT/bin/omarchy-webapp-install" \
>"$tmpdir/out" 2>"$tmpdir/err"; then
fail "interactive webapp install refuses a file: URL" "$(cat "$tmpdir/out")"
fi
grep -Fq 'must be http or https' "$tmpdir/err" ||
fail "interactive webapp install names the scheme refusal" "$(cat "$tmpdir/err")"
[[ ! -s $tmpdir/curl-log ]] ||
fail "interactive webapp install refuses before fetching the URL" "$(cat "$tmpdir/curl-log")"
[[ ! -e $(desktop_for Evil) ]] || fail "interactive webapp install writes no desktop file"
pass "interactive webapp install refuses a bad URL before fetching it"