fix(security): fail-closed XDG_RUNTIME_DIR and harden state file writes

Remove predictable /tmp/omarchy-key-visualizer.json fallback that exposed
captured key combinations and allowed symlink redirection when
XDG_RUNTIME_DIR is absent. Introduce fail-closed behaviour: disable
capture if XDG_RUNTIME_DIR is missing, equals /tmp, is not absolute,
not owned or not 0700.

Add secure_write with O_NOFOLLOW mitigation (test ! -L), atomic
tmp+rename and chmod 600 to prevent world-readable leaks and TOCTOU
races. Apply same guard to QML statePath/superPath and Panel nudge
writes (umask 077, chmod 600, symlink check).

Fixes security review at 790b07c0c9:
key-visualizer.lua:15-17,141-145
This commit is contained in:
felixzsh
2026-08-24 14:00:12 -05:00
parent 790b07c0c9
commit 89cd794708
3 changed files with 78 additions and 19 deletions
+9 -3
View File
@@ -40,7 +40,9 @@ Panel {
// the visualizer renders the move like a real keypress.
readonly property string statePath: {
var runtime = Quickshell.env("XDG_RUNTIME_DIR")
return (runtime && runtime.length > 0 ? runtime : "/tmp") + "/omarchy-key-visualizer.json"
if (!runtime || runtime.length === 0) return ""
if (runtime === "/tmp") return ""
return runtime + "/omarchy-key-visualizer.json"
}
// Config lives outside the plugin folder (the shell reloads all plugin
@@ -125,10 +127,12 @@ Panel {
var nx = Math.max(-2000, Math.min(2000, root.offsetX + dx * root.nudgeStep))
var ny = Math.max(-2000, Math.min(2000, root.offsetY + dy * root.nudgeStep))
root.writeConfig({ offsetX: nx, offsetY: ny })
if (!root.statePath || root.statePath.length === 0) return
if (root.statePath === "/tmp/omarchy-key-visualizer.json") return
var arrow = dx < 0 ? "←" : dx > 0 ? "→" : dy < 0 ? "↑" : "↓"
var now = Math.floor(Date.now() / 1000)
nudgeKeyProc.command = ["sh", "-c",
"printf '%s' '" + JSON.stringify({ keys: [arrow], t: now }) + "' > " + Util.shellQuote(root.statePath)]
"if [ ! -L " + Util.shellQuote(root.statePath) + " ]; then umask 077; printf '%s' '" + JSON.stringify({ keys: [arrow], t: now }) + "' > " + Util.shellQuote(root.statePath) + " && chmod 600 " + Util.shellQuote(root.statePath) + "; fi"]
nudgeKeyProc.running = true
nudgeReleaseTimer.interval = 120
nudgeReleaseTimer.restart()
@@ -161,9 +165,11 @@ Panel {
interval: 120
repeat: false
onTriggered: {
if (!root.statePath || root.statePath.length === 0) return
if (root.statePath === "/tmp/omarchy-key-visualizer.json") return
var now = Math.floor(Date.now() / 1000)
nudgeKeyProc.command = ["sh", "-c",
"printf '%s' '" + JSON.stringify({ keys: [], t: now }) + "' > " + Util.shellQuote(root.statePath)]
"if [ ! -L " + Util.shellQuote(root.statePath) + " ]; then umask 077; printf '%s' '" + JSON.stringify({ keys: [], t: now }) + "' > " + Util.shellQuote(root.statePath) + " && chmod 600 " + Util.shellQuote(root.statePath) + "; fi"]
nudgeKeyProc.running = true
}
}