Skip to content

Stop a model's words being a program - #63

Merged
YCistak merged 1 commit into
masterfrom
fix/harden-inputs
Aug 13, 2026
Merged

YCistak merged 1 commit into
masterfrom
fix/harden-inputs

Conversation

@YCistak

@YCistak YCistak commented Aug 13, 2026

Copy link
Copy Markdown
Owner

"Close Spotify" ends in pkill <app>, and app is whatever the language model produced from the sentence. pkill's argument is an extended regular expression, not a literal, and nothing validated it.

Measured with pgrep, which matches the same way without killing anything:

Pattern Matches
. 479 of 480 processes — the user's whole session
pylo. the running pylon daemon
-x with .* 478 — so -x alone is not the fix

The self-kill guard made it worse than it looked: isSelf compares literals while pkill matches patterns, so pylo. is not equal to "pylon", passes the guard, and then matches it.

The fix is three things, because none is enough alone

  • isProcessName — letters, digits, dot, dash, underscore; at least one alphanumeric; 64 characters. Removes the metacharacters that make the argument a program rather than a name.
  • -x — the pattern must match the whole name.
  • Dots escaped — the dot has to stay, because real names contain it (mount.ntfs-3g and python3.11 are on an ordinary desktop), and it is the one metacharacter that survives the name check. pkill -x 'pylo\.' matches nothing.

Verified against a real process table: pgrep -x 'pylo\.' → 0, pgrep -x 'pylon' → 1.

Tests drive the refusals (., .., .*, ^.*$, chrome|pylon, a b, code;rm, $(id), [a-z]+, over-long), the escaped-dot path, the self-guard evasions, and that real names still work — a guard that breaks the feature is not a fix.

Also from the same review, all smaller

  • exchange put the model's currency and coin names into URLs unescaped. The host is fixed so this reached no other server, but a stray ? or & would quietly request a different endpoint of that one. url.PathEscape and url.Values now.
  • OpenURL is bound to the frontend and opened any scheme it was handed. file:// hands a path to the desktop's default handler for whatever it turns out to be. http and https only; the check is split out so it is testable without a window.
  • The PID file is 0600, not 0644 — on Unix it sits in /tmp beside every other user's files.

Deliberately not in here

The socket: /tmp/pylon.sock, no authentication, and no client ever checks who owns it. On a shared machine another local user can bind that name first and receive pylon secret set <name> <api-key> in plaintext. It is the larger finding and needs its own change — the socket path is duplicated in pylon-ui by design, and moving it touches both modules.

🤖 Generated with Claude Code

"Close Spotify" ends in `pkill <app>`, and app is whatever the language model
produced from the sentence. pkill's argument is an extended regular
expression, not a literal, and nothing validated it.

Measured with pgrep, which matches the same way without killing anything:

  pgrep '.'          479 of 480 processes — the whole session
  pgrep 'pylo.'      the running pylon daemon
  pgrep -x '.*'      478 — so -x alone is not the fix

The self-kill guard made it worse than it looked: isSelf compares literals
while pkill matches patterns, so `pylo.` is not equal to "pylon", passes the
guard, and then matches it.

Three things together, because none of them is enough alone:

  - isProcessName: letters, digits, dot, dash, underscore, at least one
    alphanumeric, 64 characters. Removes the metacharacters that make the
    argument a program rather than a name.
  - -x, so the pattern must match the whole name.
  - dots escaped, because the dot has to stay — mount.ntfs-3g and python3.11
    are on an ordinary desktop — and it is the one metacharacter that survives
    the name check. `pkill -x 'pylo\.'` matches nothing.

Also from the same review, all smaller:

  - exchange put the model's currency and coin names into URLs unescaped. The
    host is fixed so this reached no other server, but a stray ? or & would
    quietly request a different endpoint of that one. url.PathEscape and
    url.Values now.
  - pylon-ui's OpenURL is bound to the frontend and opened any scheme it was
    handed. file:// hands a path to the desktop's default handler for whatever
    it turns out to be. http and https only, and the check is split out so it
    is testable without a window.
  - the PID file is 0600, not 0644. On Unix it sits in /tmp beside every other
    user's files.

The socket itself — /tmp, no authentication, no ownership check by clients —
is the larger finding from that review and is deliberately not in here.
@YCistak
YCistak force-pushed the fix/harden-inputs branch from 1a4a038 to adc77f6 Compare August 13, 2026 20:24
@YCistak
YCistak merged commit a2ec9db into master Aug 13, 2026
6 checks passed
@YCistak
YCistak deleted the fix/harden-inputs branch August 13, 2026 20:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant