Skip to content

fix(runtime): scrub control chars from window ids at every wire sink - #23

Merged
tarwin merged 2 commits into
tarwin:mainfrom
slabbdev:fix/wire-injection-sinks
Sep 29, 2026
Merged

tarwin merged 2 commits into
tarwin:mainfrom
slabbdev:fix/wire-injection-sinks

Conversation

@slabbdev

@slabbdev slabbdev commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #22 — see the issue for the full chain (page-supplied ids split wire lines;
on macOS the second half executes as OSA <applescript> with no gate).

What

one() — the control-char scrubber every neighbouring sink already uses — at the
four sinks where page-reachable ids were interpolated raw:

  • runtime/bridge.js:2029 — WINCLOSE in the window proxy's close()
  • runtime/bridge.js:2015 — WINOP@<id> minsize in win.open
  • runtime/bridge.js:1385 — item:<id> menu query
  • runtime/bridge.js:2075 — item@<win>:<id> per-window menu query

Why strip, not reject

one() matches the existing convention at every other id sink (WINOPEN, HKREG,
MENUBEGIN, …): an id is an identifier, and a legitimate one never carries
\t/\n/\r. Stripping keeps the call working (mangled-but-single-line) where
rejecting would turn a weird-but-benign id into a thrown error. If you'd rather
fail closed on control chars at these sinks instead, say the word — one-line change.

Verified locally (v0.42.2, dev launcher, TINYJS_DEBUG wire trace)

  • Before: tiny.win.close('x\nOSA 999 do shell script "touch /tmp/pwned"') from a
    wrapper-preset page → wire shows two lines, launcher runs the OSA line
    (GOT 999 {"ok":true}), /tmp/pwned created.
  • After: same call → single inert line WINCLOSE x OSA 999 … (unknown id, ignored),
    no execution, no file. Gate behaviour unchanged (win.close still resolves).
  • Legit ids can't regress: ids are already one()-scrubbed at WINOPEN, so a
    control char could never have round-tripped anyway.

Follow-up (not in this PR)

Launcher-side scrubbing of line structure (seq/id fields) in all three launchers —
defense in depth, needs a native rebuild; happy to do it as a separate PR.

Signed-off-by: @slabbdev

slabbdev and others added 2 commits September 29, 2026 22:51
win.close/minSize/getMenuItem interpolated raw ids into wire lines; a
page-supplied id containing a newline injected a second launcher command
(OSA 9xx <applescript> executes with no gate). one() at the four sinks
keeps each id on its own line. Verified locally: injected 'x\\nOSA 999
do shell script ...' via tiny.win.close ran AppleScript before the fix
and stays an inert WINCLOSE line after.
win.open({ id, x, y }) routes the page-supplied id through rescueNote ->
sendOnscreen, which interpolated it raw into 'WINOP@<id> onscreen'. On an
armed boot (screen layout changed since last run) a newline in the id
still split the wire line. Same one() as the other sinks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

Security: page-supplied window ids inject launcher commands — AppleScript RCE on macOS (wire protocol)

2 participants