From 877f1e96ef1af8d821a00047c3b38328eefb3b0a Mon Sep 17 00:00:00 2001 From: Taksh Date: Thu, 27 Aug 2026 07:17:44 +0530 Subject: [PATCH 1/2] Keep web app launchers on http(s) Chromium --app= will run javascript:, file:, and data: URLs. Prefix schemeless input with https as before, then refuse anything else. --- bin/omarchy-webapp-install | 32 +++++++++++---- test/shell.d/webapp-install-test.sh | 60 +++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 test/shell.d/webapp-install-test.sh diff --git a/bin/omarchy-webapp-install b/bin/omarchy-webapp-install index acfdf858..308f8841 100755 --- a/bin/omarchy-webapp-install +++ b/bin/omarchy-webapp-install @@ -42,6 +42,27 @@ 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). Desktop-file value escaping is a separate concern +# (see open work on the freedesktop string/Exec rules). +normalize_webapp_url() { + local url=$1 + if [[ ! $url =~ ^[a-zA-Z][a-zA-Z0-9+.-]*: ]]; then + url="https://$url" + fi + printf '%s' "$url" +} + +require_http_url() { + local url=$1 + if [[ $url =~ ^https?:// ]]; then + return 0 + fi + echo "Error: web app URL must be http or https." >&2 + exit 1 +} + fetch_site_icon() { local site_url="$1" dest="$2" local origin page icon_url @@ -69,9 +90,7 @@ if (( $# < 3 )); then echo -e "\e[32mLet's create a new web app you can start with the app launcher.\n\e[0m" APP_NAME=$(gum input --prompt "Name> " --placeholder "My favorite web app") 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") # Try to fetch the site's icon automatically first. mkdir -p "$ICON_DIR" @@ -88,10 +107,7 @@ 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") ICON_REF="$3" CUSTOM_EXEC="$4" # Optional custom exec command MIME_TYPES="$5" # Optional mime types @@ -104,6 +120,8 @@ if [[ -z $APP_NAME || -z $APP_URL ]]; then exit 1 fi +require_http_url "$APP_URL" + if [[ -z $ICON_REF ]]; then ICON_VALUE=$(safe_icon_name "$APP_NAME") mkdir -p "$ICON_DIR" diff --git a/test/shell.d/webapp-install-test.sh b/test/shell.d/webapp-install-test.sh new file mode 100644 index 00000000..f436a2d3 --- /dev/null +++ b/test/shell.d/webapp-install-test.sh @@ -0,0 +1,60 @@ +#!/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" From 93824100269bb1b08e85fe86f103b40d5caaf611 Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Thu, 27 Aug 2026 20:10:15 +0200 Subject: [PATCH 2/2] Validate the web app URL before anything fetches it The scheme check ran after the interactive branch, which had already handed the URL to curl: a refused `file://` or `ftp://` URL was dereferenced first, the whole string went out to Google's favicon endpoint in a `domain=` query, and a fetch that happened to succeed left an orphan icon and a rebuilt icon cache behind. Validating immediately after normalization puts the refusal ahead of every dereference in both branches. A leading space also kept the URL out of the scheme test entirely. `normalize_webapp_url` saw no scheme, prefixed it, and ` file:///etc/passwd` became `https:// file:///etc/passwd`, which passes `^https?://`. The desktop `Exec` field is unquoted, so it splits back into two arguments, and `omarchy-launch-webapp` forwards everything after the first to the browser, where a bare argument is another URL to open. Refusing whitespace closes that without touching desktop-entry escaping. Scheme comparison is case-insensitive because schemes are: `HTTPS://example.com` installed before this check existed and has no reason to stop. Co-Authored-By: Claude Opus 5 (1M context) Co-Authored-By: Codex XHigh --- bin/omarchy-webapp-install | 20 +++++++--- test/shell.d/webapp-install-test.sh | 60 +++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/bin/omarchy-webapp-install b/bin/omarchy-webapp-install index 308f8841..ebf6ea39 100755 --- a/bin/omarchy-webapp-install +++ b/bin/omarchy-webapp-install @@ -54,13 +54,21 @@ normalize_webapp_url() { printf '%s' "$url" } +# A space in the URL becomes a second desktop Exec argument, which +# omarchy-launch-webapp hands to the browser as another URL to open. Schemes +# are case-insensitive. require_http_url() { local url=$1 - if [[ $url =~ ^https?:// ]]; then - return 0 + + 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 - echo "Error: web app URL must be http or https." >&2 - exit 1 } fetch_site_icon() { @@ -91,6 +99,7 @@ if (( $# < 3 )); then APP_NAME=$(gum input --prompt "Name> " --placeholder "My favorite web app") APP_URL=$(gum input --prompt "URL> " --placeholder "https://example.com") 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" @@ -108,6 +117,7 @@ if (( $# < 3 )); then else APP_NAME="$1" 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 @@ -120,8 +130,6 @@ if [[ -z $APP_NAME || -z $APP_URL ]]; then exit 1 fi -require_http_url "$APP_URL" - if [[ -z $ICON_REF ]]; then ICON_VALUE=$(safe_icon_name "$APP_NAME") mkdir -p "$ICON_DIR" diff --git a/test/shell.d/webapp-install-test.sh b/test/shell.d/webapp-install-test.sh index f436a2d3..f845d928 100644 --- a/test/shell.d/webapp-install-test.sh +++ b/test/shell.d/webapp-install-test.sh @@ -58,3 +58,63 @@ for url in "javascript:alert(1)" "file:///etc/passwd" "data:text/html,hi" "ftp:/ [[ ! -e $(desktop_for Bad) ]] || fail "webapp install does not write a desktop file for '$url'" done pass "webapp install refuses non-http(s) URLs" + +# A leading space keeps the URL out of the scheme test, and the desktop Exec +# field splits it back into two arguments the browser both opens. +for url in " javascript:alert(1)" " file:///etc/passwd" "https://example.com data:text/html,hi"; 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"