Files
omarchycn/bin/omarchy-theme-color
0ad64a59df Fix command injection in theme install, drop tzupdate NOPASSWD (#6694)
* Fix theme install code execution and drop tzupdate NOPASSWD

VULN-01 (C, D, E): a malicious theme can execute arbitrary code
during install through three injection sinks:

  C: colors.toml values reach a sed script unsanitized.
     GNU sed's `e` flag runs the pattern space as a shell command.

  D: vscode.json `.name` is interpolated into a sed replacement
     string without escaping sed metacharacters.

  E: keyboard.rgb content is interpolated into a python3 -c
     argument without validation.

Fix C by validating keys and values in omarchy-theme-color's parser
with a character allowlist. Byte-identical output for all 22 shipped
themes.

Fix D by escaping backslash, ampersand, and slash in the theme name
before sed interpolation.

Fix E by gating on ^[0-9A-Fa-f]{6}$ before interpolation, in both
the Framework 16 and ASUS ROG keyboard scripts.

VULN-02: the tzupdate sudoers grant has no argument constraint.
tzupdate -l lets any wheel user write a root-owned symlink to any
path. Drop it; nothing has invoked tzupdate since omarchy-cmd-tzupdate
was removed. Keep timedatectl set-timezone.

* Harden keyboard and vscode theme scripts

keyboard-f16: pass hex as sys.argv instead of interpolating into
python3 -c. The hex validation gate stays as the primary defense;
argv separation is defense-in-depth per OWASP guidance.

vscode: replace sed interpolation of theme name with jq, which
handles arbitrary strings safely via --arg. Validate extension IDs
against ^[a-zA-Z0-9._-]+$ before passing to --install-extension.

* Keep VS Code settings edits JSONC-safe

settings.json is JSONC, so routing the write through jq dropped theme sync
entirely for anyone with a comment or trailing comma in the file, including
the `{ "workbench.colorTheme": "",\n}` shape Omarchy itself creates. Edit in
place again and close the injection by validating the theme label instead.

Scope the extension-id guard to the install so a malformed id no longer skips
the colorTheme write, and treat a missing descriptor field as empty rather
than the literal string "null".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Widen the accepted colors.toml value charset

The sanitizer dropped gradient angles, decimals, underscored palette
references, and paths, which vanish from --raw/--all and leave a raw
{{ placeholder }} in the generated config. Allow the punctuation real
palettes use, keep out everything sed treats as special, and say so on
stderr rather than dropping a key silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: David Heinemeier Hansson <david@hey.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 12:31:52 +02:00

305 lines
10 KiB
Bash
Executable File

#!/bin/bash
# omarchy:summary=Resolve semantic colors from an Omarchy theme colors.toml
# omarchy:args=[--file <colors.toml>] (--all | --raw | <key> [fallback])
# omarchy:hidden=true
# Shared colors.toml parser/resolver. The alias/fallback cascade mirrors what
# omarchy-theme-set-templates bakes into the generated configs at theme-set
# time, so every consumer (templates, OSC sequences, tmux, GNOME, previews)
# resolves the exact same palette.
#
# --all print every resolved key<TAB>value pair (sorted by key)
# --raw print only the key<TAB>value pairs defined in the file
# <key> [fallback] print one resolved value; the fallback is tried as
# another palette key first, then used verbatim
#
# Theme mode precedence: `mode` key, legacy `theme_type` key, a light.mode
# file beside the colors.toml, background luminance auto-detect, dark.
COLORS_FILE="$HOME/.local/state/omarchy/current/theme/colors.toml"
OUTPUT=""
QUERY_KEY=""
QUERY_FALLBACK=""
usage() {
echo "Usage: omarchy-theme-color [--file <colors.toml>] (--all | --raw | <key> [fallback])"
}
while (( $# > 0 )); do
case "$1" in
--file)
COLORS_FILE="${2:-}"
shift 2
;;
--all)
OUTPUT="all"
shift
;;
--raw)
OUTPUT="raw"
shift
;;
-h | --help)
usage
exit 0
;;
*)
if [[ -z $QUERY_KEY ]]; then
QUERY_KEY="$1"
elif [[ -z $QUERY_FALLBACK ]]; then
QUERY_FALLBACK="$1"
else
usage >&2
exit 1
fi
shift
;;
esac
done
if [[ -z $OUTPUT && -z $QUERY_KEY ]] || [[ -n $OUTPUT && -n $QUERY_KEY ]]; then
usage >&2
exit 1
fi
COLORS_DIR=$(dirname "$COLORS_FILE")
declare -A THEME_COLORS
# Mix two hex colors. Amount may be a fraction (0.30) or percentage (30%).
mix_color() {
local start="${1#\#}"
local end="${2#\#}"
local amount="$3"
awk -v start="$start" -v end="$end" -v amount="$amount" '
function hex_value(char) {
return index("0123456789abcdef", tolower(char)) - 1
}
function hex_pair_to_int(hex, idx) {
return hex_value(substr(hex, idx, 1)) * 16 + hex_value(substr(hex, idx + 1, 1))
}
BEGIN {
if (amount ~ /%$/) {
sub(/%$/, "", amount)
amount = amount / 100
} else {
amount += 0
if (amount > 1) amount = amount / 100
}
if (amount < 0) amount = 0
if (amount > 1) amount = 1
start_r = hex_pair_to_int(start, 1)
start_g = hex_pair_to_int(start, 3)
start_b = hex_pair_to_int(start, 5)
end_r = hex_pair_to_int(end, 1)
end_g = hex_pair_to_int(end, 3)
end_b = hex_pair_to_int(end, 5)
red = int(start_r * (1 - amount) + end_r * amount + 0.5)
green = int(start_g * (1 - amount) + end_g * amount + 0.5)
blue = int(start_b * (1 - amount) + end_b * amount + 0.5)
printf "#%02x%02x%02x\n", red, green, blue
}
'
}
alias_theme_color() {
local key="$1"
local fallback="$2"
[[ ${THEME_COLORS[$key]} ]] || THEME_COLORS[$key]="${THEME_COLORS[$fallback]}"
}
resolve_theme_mode() {
local bg_hex lum
[[ ${THEME_COLORS[mode]} ]] || THEME_COLORS[mode]="${THEME_COLORS[theme_type]}"
[[ ${THEME_COLORS[mode]} ]] && return
if [[ -f $COLORS_DIR/light.mode ]]; then
THEME_COLORS[mode]="light"
elif [[ ${THEME_COLORS[background]} =~ ^#[0-9A-Fa-f]{6}$ ]]; then
bg_hex="${THEME_COLORS[background]#\#}"
lum=$(( $(printf "%d" "0x${bg_hex:0:2}") + $(printf "%d" "0x${bg_hex:2:2}") + $(printf "%d" "0x${bg_hex:4:2}") ))
(( lum > 382 )) && THEME_COLORS[mode]="light" || THEME_COLORS[mode]="dark"
else
THEME_COLORS[mode]="dark"
fi
}
parse_colors_file() {
local key value
[[ -f $COLORS_FILE ]] || return 0
while IFS='=' read -r key value; do
key="${key//[\"\' ]/}" # strip quotes and spaces from key
[[ $key && $key != \#* ]] || continue # skip empty lines and comments
if [[ $value == *[\"\']* ]]; then
value="${value#*[\"\']}"
value="${value%%[\"\']*}" # extract value between quotes (ignores inline comments)
else
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}" # trim unquoted values
fi
# Values reach consumers as sed replacement text, so the charset excludes
# the delimiter, backslash, and & while still covering everything a real
# palette holds: hex, rgb()/rgba() lists, gradient angles like -45deg,
# decimals, and bare words. Rejections are announced so a third-party theme
# doesn't lose a key silently and leave a raw {{ placeholder }} behind.
if [[ ! $key =~ ^[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-]+$ ]]; then
printf 'omarchy-theme-color: skipping key with unsupported characters\n' >&2
continue
fi
if [[ ! $value =~ ^[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#(),._+/%\ -]*$ ]]; then
printf 'omarchy-theme-color: skipping %s: unsupported characters in value\n' "$key" >&2
continue
fi
THEME_COLORS[$key]="$value"
[[ $OUTPUT == "raw" ]] && printf '%s\t%s\n' "$key" "$value"
done <"$COLORS_FILE"
return 0
}
resolve_theme_colors() {
local key
# Accept the complete legacy short-name palette before applying ANSI
# fallbacks or deriving shades. Canonical names take precedence when a theme
# defines both forms.
declare -A legacy_palette_alias=(
[background]=bg
[dark_background]=dark_bg
[darker_background]=darker_bg
[lighter_background]=lighter_bg
[foreground]=fg
[dark_foreground]=dark_fg
[light_foreground]=light_fg
[bright_foreground]=bright_fg
)
for key in "${!legacy_palette_alias[@]}"; do
alias_theme_color "$key" "${legacy_palette_alias[$key]}"
done
# Themes generated before the semantic palette may only define ANSI names.
[[ ${THEME_COLORS[background]} ]] || THEME_COLORS[background]="${THEME_COLORS[color0]}"
[[ ${THEME_COLORS[foreground]} ]] || THEME_COLORS[foreground]="${THEME_COLORS[color7]}"
[[ ${THEME_COLORS[background]} ]] && THEME_COLORS[color0]="${THEME_COLORS[background]}"
[[ ${THEME_COLORS[foreground]} ]] && THEME_COLORS[color7]="${THEME_COLORS[foreground]}"
# Legacy compatibility: map ANSI color0..color15 to semantic names.
declare -A legacy_alias=(
[red]=color1
[green]=color2
[yellow]=color3
[blue]=color4
[magenta]=color5
[cyan]=color6
[bright_red]=color9
[bright_green]=color10
[bright_yellow]=color11
[bright_blue]=color12
[bright_magenta]=color13
[bright_cyan]=color14
)
for key in "${!legacy_alias[@]}"; do
alias_theme_color "$key" "${legacy_alias[$key]}"
done
alias_theme_color magenta purple
alias_theme_color bright_magenta bright_purple
[[ ${THEME_COLORS[light_foreground]} ]] || THEME_COLORS[light_foreground]="${THEME_COLORS[color7]:-${THEME_COLORS[foreground]}}"
[[ ${THEME_COLORS[bright_foreground]} ]] || THEME_COLORS[bright_foreground]="${THEME_COLORS[color15]:-${THEME_COLORS[foreground]}}"
THEME_COLORS[cursor]="${THEME_COLORS[bright_foreground]}"
[[ ${THEME_COLORS[lighter_background]} ]] || THEME_COLORS[lighter_background]="${THEME_COLORS[color0]:-${THEME_COLORS[background]}}"
[[ ${THEME_COLORS[dark_foreground]} ]] || THEME_COLORS[dark_foreground]="${THEME_COLORS[color8]:-${THEME_COLORS[foreground]}}"
[[ ${THEME_COLORS[muted]} ]] || THEME_COLORS[muted]="${THEME_COLORS[color8]:-${THEME_COLORS[dark_foreground]}}"
[[ ${THEME_COLORS[selection]} ]] || THEME_COLORS[selection]="${THEME_COLORS[selection_background]:-${THEME_COLORS[color8]:-${THEME_COLORS[color0]:-${THEME_COLORS[background]}}}}"
[[ ${THEME_COLORS[selection_background]} ]] || THEME_COLORS[selection_background]="${THEME_COLORS[selection]}"
[[ ${THEME_COLORS[selection_foreground]} ]] || THEME_COLORS[selection_foreground]="${THEME_COLORS[bright_foreground]}"
[[ ${THEME_COLORS[orange]} ]] || THEME_COLORS[orange]="${THEME_COLORS[yellow]}"
[[ ${THEME_COLORS[brown]} ]] || THEME_COLORS[brown]=$(mix_color "${THEME_COLORS[orange]}" "#000000" 50%)
# Auto-derive shades from base accents when not defined and not aliased from colorN
[[ ${THEME_COLORS[dark_background]} ]] || THEME_COLORS[dark_background]=$(mix_color "${THEME_COLORS[background]}" "#000000" 25%)
[[ ${THEME_COLORS[darker_background]} ]] || THEME_COLORS[darker_background]=$(mix_color "${THEME_COLORS[background]}" "#000000" 50%)
[[ ${THEME_COLORS[bright_red]} ]] || THEME_COLORS[bright_red]=$(mix_color "${THEME_COLORS[red]}" "#ffffff" 20%)
[[ ${THEME_COLORS[bright_yellow]} ]] || THEME_COLORS[bright_yellow]=$(mix_color "${THEME_COLORS[yellow]}" "#ffffff" 20%)
[[ ${THEME_COLORS[bright_green]} ]] || THEME_COLORS[bright_green]=$(mix_color "${THEME_COLORS[green]}" "#ffffff" 20%)
[[ ${THEME_COLORS[bright_cyan]} ]] || THEME_COLORS[bright_cyan]=$(mix_color "${THEME_COLORS[cyan]}" "#ffffff" 20%)
[[ ${THEME_COLORS[bright_blue]} ]] || THEME_COLORS[bright_blue]=$(mix_color "${THEME_COLORS[blue]}" "#ffffff" 20%)
[[ ${THEME_COLORS[bright_magenta]} ]] || THEME_COLORS[bright_magenta]=$(mix_color "${THEME_COLORS[magenta]}" "#ffffff" 20%)
alias_theme_color purple magenta
alias_theme_color bright_purple bright_magenta
# Keep semantic themes compatible with consumers that still reference the
# legacy ANSI names directly.
declare -A ansi_alias=(
[color0]=background
[color1]=red
[color2]=green
[color3]=yellow
[color4]=blue
[color5]=magenta
[color6]=cyan
[color7]=foreground
[color8]=muted
[color9]=bright_red
[color10]=bright_green
[color11]=bright_yellow
[color12]=bright_blue
[color13]=bright_magenta
[color14]=bright_cyan
[color15]=bright_foreground
)
for key in "${!ansi_alias[@]}"; do
alias_theme_color "$key" "${ansi_alias[$key]}"
done
# Keep canonical themes compatible with old user templates and consumers
# that still query the short palette names directly.
for key in "${!legacy_palette_alias[@]}"; do
if [[ ${THEME_COLORS[$key]} ]]; then
THEME_COLORS["${legacy_palette_alias[$key]}"]="${THEME_COLORS[$key]}"
fi
done
resolve_theme_mode
THEME_COLORS[theme_type]="${THEME_COLORS[mode]}"
}
parse_colors_file
if [[ $OUTPUT == "raw" ]]; then
exit 0
fi
resolve_theme_colors
if [[ $OUTPUT == "all" ]]; then
while IFS= read -r key; do
printf '%s\t%s\n' "$key" "${THEME_COLORS[$key]}"
done < <(printf '%s\n' "${!THEME_COLORS[@]}" | LC_ALL=C sort)
exit 0
fi
value="${THEME_COLORS[$QUERY_KEY]:-}"
if [[ -z $value && -n $QUERY_FALLBACK ]]; then
value="${THEME_COLORS[$QUERY_FALLBACK]:-$QUERY_FALLBACK}"
fi
[[ -n $value ]] || exit 1
printf '%s\n' "$value"