Files
omarchycn/bin/omarchy-theme-set-vscode
T
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

154 lines
5.3 KiB
Bash
Executable File

#!/bin/bash
# omarchy:summary=Sync Omarchy theme to VS Code, VSCodium, and Cursor
# omarchy:hidden=true
VS_CODE_THEME_DESCRIPTOR="$HOME/.local/state/omarchy/current/theme/vscode.json"
GENERATED_THEME="$HOME/.local/state/omarchy/current/theme/vscode-theme.json"
GENERATED_EXTENSION_NAME="omarchy-theme"
GENERATED_EXTENSION_ID="local.$GENERATED_EXTENSION_NAME"
GENERATED_EXTENSION_VERSION="1.0.0"
register_generated_extension() {
local ext_dir="$1"
local ext_base extensions_file obsolete_file relative tmp
ext_base=$(dirname "$ext_dir")
extensions_file="$ext_base/extensions.json"
obsolete_file="$ext_base/.obsolete"
relative=$(basename "$ext_dir")
if [[ -f $obsolete_file ]]; then
tmp=$(mktemp)
if jq --arg key "$GENERATED_EXTENSION_ID-$GENERATED_EXTENSION_VERSION" 'del(.[$key])' "$obsolete_file" >"$tmp"; then
mv "$tmp" "$obsolete_file"
[[ $(jq 'length' "$obsolete_file") == "0" ]] && rm -f "$obsolete_file"
else
rm -f "$tmp"
fi
fi
[[ -f $extensions_file ]] || printf '[]\n' >"$extensions_file"
tmp=$(mktemp)
if jq \
--arg id "$GENERATED_EXTENSION_ID" \
--arg version "$GENERATED_EXTENSION_VERSION" \
--arg fs_path "$ext_dir" \
--arg external "file://$ext_dir" \
--arg relative "$relative" \
'map(select(.identifier.id != $id)) + [{
identifier: { id: $id },
version: $version,
location: {
"$mid": 1,
fsPath: $fs_path,
external: $external,
path: $fs_path,
scheme: "file"
},
relativeLocation: $relative
}]' \
"$extensions_file" >"$tmp"; then
mv "$tmp" "$extensions_file"
else
rm -f "$tmp"
fi
}
# Install generated theme as a local extension for a given editor
install_generated_extension() {
local ext_dir="$1"
local theme_type ui_theme
theme_type=$(jq -r '.type // "dark"' "$GENERATED_THEME")
if [[ $theme_type == "light" ]]; then
ui_theme="vs"
else
ui_theme="vs-dark"
fi
mkdir -p "$ext_dir/themes"
ln -sfn "$GENERATED_THEME" "$ext_dir/themes/omarchy-color-theme.json"
cat > "$ext_dir/package.json" <<EOF
{
"name": "$GENERATED_EXTENSION_NAME",
"displayName": "Omarchy",
"description": "Omarchy color theme",
"publisher": "local",
"version": "$GENERATED_EXTENSION_VERSION",
"engines": { "vscode": "^1.70.0" },
"categories": ["Themes"],
"contributes": {
"themes": [{
"label": "Omarchy",
"uiTheme": "$ui_theme",
"path": "./themes/omarchy-color-theme.json"
}]
}
}
EOF
register_generated_extension "$ext_dir"
}
set_theme() {
local editor_cmd="$1"
local settings_path="$2"
local ext_base="$3"
omarchy-cmd-present "$editor_cmd" || return 0
local theme_name=""
if [[ -f "$VS_CODE_THEME_DESCRIPTOR" ]]; then
# Theme specifies a preferred 3rd-party extension/theme pair. Both fields
# come from the theme, so neither is trusted as a shell or sed argument.
local extension
theme_name=$(jq -r '.name // empty' "$VS_CODE_THEME_DESCRIPTOR")
extension=$(jq -r '.extension // empty' "$VS_CODE_THEME_DESCRIPTOR")
# A label with quotes, backslashes, or control characters can't land in
# settings.json as valid JSON, so drop it rather than write it out.
[[ $theme_name =~ ^[^[:cntrl:]\"\\]+$ ]] || theme_name=""
# Only install a well-formed publisher.name id; a malformed one skips the
# install without skipping the colorTheme write below.
if [[ $extension =~ ^[a-zA-Z0-9._-]+$ ]] &&
! "$editor_cmd" --list-extensions 2>/dev/null | grep -Fxq "$extension"; then
"$editor_cmd" --install-extension "$extension" >/dev/null 2>&1
fi
elif [[ -f "$GENERATED_THEME" ]]; then
# VS Code only discovers local color themes through an extension package.
install_generated_extension "$ext_base/$GENERATED_EXTENSION_NAME"
theme_name="Omarchy"
fi
if [[ -n "$theme_name" ]]; then
mkdir -p "$(dirname "$settings_path")"
[[ -f $settings_path ]] || printf '{\n}\n' >"$settings_path"
# settings.json is JSONC: comments and trailing commas are legal there and
# common in practice, so it's edited in place rather than round-tripped
# through a strict JSON parser that would reject it and reformat the rest.
local escaped=${theme_name//&/\\&}
escaped=${escaped//|/\\|}
if ! grep -q '"workbench.colorTheme"' "$settings_path"; then
sed -i --follow-symlinks -E '0,/\{/{s/\{/{\ "workbench.colorTheme": "",/}' "$settings_path"
fi
sed -i --follow-symlinks -E \
"s|(\"workbench.colorTheme\"[[:space:]]*:[[:space:]]*\")[^\"]*(\")|\1$escaped\2|" \
"$settings_path"
elif [[ -f $settings_path ]]; then
sed -i --follow-symlinks -E 's/\"workbench\.colorTheme\"[[:space:]]*:[^,}]*,?//' "$settings_path"
fi
}
! omarchy-toggle-enabled skip-vscode-theme-changes && set_theme "code" "$HOME/.config/Code/User/settings.json" "$HOME/.vscode/extensions"
! omarchy-toggle-enabled skip-vscode-insiders-theme-changes && set_theme "code-insiders" "$HOME/.config/Code - Insiders/User/settings.json" "$HOME/.vscode-insiders/extensions"
! omarchy-toggle-enabled skip-codium-theme-changes && set_theme "codium" "$HOME/.config/VSCodium/User/settings.json" "$HOME/.vscode-oss/extensions"
! omarchy-toggle-enabled skip-cursor-theme-changes && set_theme "cursor" "$HOME/.config/Cursor/User/settings.json" "$HOME/.cursor/extensions"