Closes the unauthenticated remote-code-execution chain found in a security audit and continues through the rest of the audit's findings, adding web-based macro authoring and a visual redesign. 7 commits, grouped by concern.
Security
P0 — unauthenticated RCE closed. Mandatory per-install auth token on all /api/* routes and the /ws handshake (timing-safe compare); PWA shell stays public to bootstrap. Macro app commands no longer run through a shell (shell=True removed; shlex-tokenized, exec'd directly) — kills ;/&&/|/$()/redirection injection. LAN binding is configurable (web.allow_lan). Image path-traversal in /api/image fixed. Anti-clickjacking response headers.
Relay server hardened. WS auth brute-force lockout + socket close; IP rate-limiting and Origin allowlist on WS upgrades; /health no longer leaks session IDs; credentials removed from URLs (header-based); sessions bounded + TTL-pruned; 12-char session IDs with rejection sampling; atomic/debounced session store; helmet CSP, trust proxy, restricted CORS; sane .env.example.
Web client. DOM XSS eliminated (DOM construction only — no macro/tab value reaches innerHTML), strict CSP, no inline handlers, JSON.parse guarded.
Desktop/backend. Relay password moved to the OS keyring (0600 JSON fallback); settings file chmod 0600.
Robustness
Macro store: lock-guarded reads/writes, atomic macros.json writes, execution moved outside the lock, debounced last_used, key/wait validation.
Web server: WebSocket leak fixed, dead-socket pruning on broadcast, /api/execute offloaded off the event loop, image upload hardened (size cap, Pillow validation, server-generated name, relative path only).
GUI: macro execution off the Qt main thread, debounced resize, named delete dialogs, minimize_to_tray honored.
PWA: service worker network-first for the app shell, WS reconnect backoff with teardown, tab-switch race guarded.
Features & design
Web-based macro creation/editing — a "+" command-sequence builder in the PWA (previously execute-only).
Media/system keys (volume, play/pause, track nav) in the macro editor.
End-to-end against the real FastAPI server in a headless browser: token auth (401/200), WebSocket connect, macro created through the web editor (correct schema, atomic persist, live refresh), and the shell-injection RCE confirmed closed (a redirection payload did not execute). Relay TypeScript type-checks clean; Python compiles; concurrent macro-store writes verified deadlock-free.
Review round (included in this branch)
An automated completeness review of the branch confirmed all intended changes are present and the P0 RCE chain is genuinely closed, and caught two Major issues that were then fixed here:
Relay lockout DoS — the auth lockout was keyed on sessionId alone and shared between the web/desktop paths, letting anyone with the share link lock out the owner and block the desktop. Now namespaced per-path and per-IP.
Relay web client XSS — the DOM-XSS/CSP hardening had reached the local PWA but not the internet-facing macropad-relay/public/app.html. Now rebuilt with safe DOM construction, externalized scripts, and script-src 'self'.
All minor findings were also addressed (credentials out of local image URLs, local auth-failure state, tightened connect-src, relay enumeration-oracle removal, atomic session cap, backoff-after-auth, ALLOWED_ORIGINS deploy note). The relay web client is intentionally kept execute-only (documented in the README).
Not run interactively
The PySide6 desktop GUI and the live relay were reviewed + compile/type-checked but not exercised at runtime (no desktop session / relay deployment here) — worth a manual smoke test.
## Summary
Closes the unauthenticated remote-code-execution chain found in a security audit and continues through the rest of the audit's findings, adding web-based macro authoring and a visual redesign. 7 commits, grouped by concern.
## Security
- **P0 — unauthenticated RCE closed.** Mandatory per-install auth token on all `/api/*` routes and the `/ws` handshake (timing-safe compare); PWA shell stays public to bootstrap. Macro `app` commands no longer run through a shell (`shell=True` removed; `shlex`-tokenized, exec'd directly) — kills `;`/`&&`/`|`/`$()`/redirection injection. LAN binding is configurable (`web.allow_lan`). Image path-traversal in `/api/image` fixed. Anti-clickjacking response headers.
- **Relay server hardened.** WS auth brute-force lockout + socket close; IP rate-limiting and Origin allowlist on WS upgrades; `/health` no longer leaks session IDs; credentials removed from URLs (header-based); sessions bounded + TTL-pruned; 12-char session IDs with rejection sampling; atomic/debounced session store; helmet CSP, trust proxy, restricted CORS; sane `.env.example`.
- **Web client.** DOM XSS eliminated (DOM construction only — no macro/tab value reaches `innerHTML`), strict CSP, no inline handlers, `JSON.parse` guarded.
- **Desktop/backend.** Relay password moved to the OS keyring (0600 JSON fallback); settings file chmod 0600.
## Robustness
- Macro store: lock-guarded reads/writes, atomic `macros.json` writes, execution moved outside the lock, debounced `last_used`, key/wait validation.
- Web server: WebSocket leak fixed, dead-socket pruning on broadcast, `/api/execute` offloaded off the event loop, image upload hardened (size cap, Pillow validation, server-generated name, relative path only).
- GUI: macro execution off the Qt main thread, debounced resize, named delete dialogs, `minimize_to_tray` honored.
- PWA: service worker network-first for the app shell, WS reconnect backoff with teardown, tab-switch race guarded.
## Features & design
- **Web-based macro creation/editing** — a "+" command-sequence builder in the PWA (previously execute-only).
- **Media/system keys** (volume, play/pause, track nav) in the macro editor.
- **"Tactile Deck" visual redesign** — semantic design tokens, light/dark themes, per-macro colors, accessible cards/tabs (roles, focus rings, reduced-motion).
- README updated to match.
## Verification
End-to-end against the real FastAPI server in a headless browser: token auth (401/200), WebSocket connect, macro created through the web editor (correct schema, atomic persist, live refresh), and the shell-injection RCE confirmed closed (a redirection payload did not execute). Relay TypeScript type-checks clean; Python compiles; concurrent macro-store writes verified deadlock-free.
## Review round (included in this branch)
An automated completeness review of the branch confirmed all intended changes are present and the P0 RCE chain is genuinely closed, and caught two Major issues that were then fixed here:
- **Relay lockout DoS** — the auth lockout was keyed on `sessionId` alone and shared between the web/desktop paths, letting anyone with the share link lock out the owner and block the desktop. Now namespaced per-path and per-IP.
- **Relay web client XSS** — the DOM-XSS/CSP hardening had reached the local PWA but not the internet-facing `macropad-relay/public/app.html`. Now rebuilt with safe DOM construction, externalized scripts, and `script-src 'self'`.
All minor findings were also addressed (credentials out of local image URLs, local auth-failure state, tightened `connect-src`, relay enumeration-oracle removal, atomic session cap, backoff-after-auth, `ALLOWED_ORIGINS` deploy note). The relay web client is intentionally kept execute-only (documented in the README).
## Not run interactively
The PySide6 desktop GUI and the live relay were reviewed + compile/type-checked but not exercised at runtime (no desktop session / relay deployment here) — worth a manual smoke test.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The local FastAPI server previously exposed macro create/execute over the
network with no authentication while bound to 0.0.0.0, and macro `app`
commands ran via subprocess with shell=True — an unauthenticated RCE for
anyone on the LAN (and, via the relay, the internet).
- Mandatory per-install auth token (secrets.token_urlsafe) required on all
/api/* routes and the /ws handshake; timing-safe hmac.compare_digest.
PWA shell + /static assets remain public to bootstrap.
- Token auto-generated and persisted in settings.json (now chmod 0600).
- Bind host is configurable (web.allow_lan); token is the security boundary.
- macro `app` launch no longer uses shell=True — commands are tokenized with
shlex and exec'd directly, killing ;/&&/|/$() shell chaining.
- Relay client injects the local token and only forwards /api/* paths, and no
longer forwards attacker-supplied headers to the local server.
- Fixed path traversal in /api/image/{path} (realpath-confined to macro_images).
- GUI embeds the token in the LAN URL/QR; PWA reads it and sends it on API,
WebSocket, and image requests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes the account-takeover chain and related issues found in audit:
- WS auth brute-force protection: per-session lockout (authThrottle) + per-
socket failure cap that closes the socket (1008); applied to both web-client
and desktop auth. Failed auth no longer leaves the socket open for unlimited
guesses.
- WS upgrades now pass an IP-based rate limiter and Origin allowlist before
handleUpgrade (previously bypassed all HTTP middleware); trust proxy set so
limiting keys on the real client IP.
- /health no longer leaks live session IDs (counts only).
- .env.example rate-limit fixed (900000ms / 300) from the accidental ~11k rps.
- Credentials moved out of URLs into the X-MacroPad-Password header; images
fetched via header + blob URLs; login stores creds in sessionStorage.
- Session creation bounded (max sessions, min password length) and TTL-pruned;
session store writes are now atomic (temp+rename) and debounced.
- Session IDs lengthened to 12 chars with rejection sampling (no modulo bias).
- Enable helmet CSP; restrict CORS to configured origins.
- Request IDs use crypto.randomUUID; drop postinstall build hook and uuid dep.
- Uniform response for unknown session IDs (removes enumeration oracle).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Security:
- Eliminate DOM-based XSS: renderMacros/renderTabs rebuilt with
createElement/textContent/setAttribute + addEventListener. No macro name,
id, image_path, or category ever reaches innerHTML.
- Remove all inline on*= handlers; add a strict Content-Security-Policy meta
(script-src 'self', object-src 'none', base-uri 'none', frame-ancestors
'none').
- Guard JSON.parse on WebSocket frames.
Robustness:
- Service worker: network-first for the app shell + JS/CSS so fixes actually
ship; synthetic 503 for offline API calls; drop no-op /ws intercept; bump
cache version.
- WebSocket reconnect: exponential backoff (1s→30s), tear down old socket
before reconnect, stop on pagehide/beforeunload.
- Tab-switch race fixed with a request-id guard; executing state cleared in
finally.
Accessibility:
- Macro cards and tabs are real buttons with aria-labels/roles; toast +
connection status are aria-live; removed user-scalable=no.
Feature — web-based macro creation/editing:
- Floating "+" button + modal command-sequence builder (text/key/hotkey/
wait/app) with reorder/remove; create via POST, edit via PUT (prefilled
from GET /api/macro/{id}), delete with a named confirm. Works in both local
and relay auth modes. Empty state now offers "Create your first macro".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
macro_manager:
- All macro reads/writes guarded by a lock; macros.json written atomically
(temp + fsync + os.replace). Commands are copied under the lock and executed
outside it so wait/keyboard steps don't block other threads.
- last_used disk writes debounced (>=5s) instead of a full rewrite per press.
- Validate key/hotkey names against pyautogui.KEYBOARD_KEYS and clamp wait ms;
invalid input is skipped/logged, not blindly executed.
web_server:
- WebSocket receive loop always disconnects (fixes dead-socket leak); broadcast
prunes failed sockets.
- POST /api/execute offloaded to a thread executor so a long macro no longer
blocks the event loop.
- Image upload hardened: 5MB cap (413), real-image validation via Pillow,
server-generated name under macro_images/, returns a relative path only,
cleans up on failure.
gui:
- Execute macros off the Qt main thread (signal back to status bar); debounce
resizeEvent and only rebuild when the column count changes.
- Relay password stored in the OS keyring when available (plaintext JSON blanked;
graceful fallback if no backend); delete dialogs name the macro; honor the
minimize-to-tray setting; surface save failures.
- Add media/system keys (volume, play/pause, track nav) to the macro editor.
- THEME gains shared danger/accent-hover colors; drop dead imports.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Full semantic design-token layer (surfaces, spacing scale, radii, elevation,
type scale) as the single source of truth; electric-violet signature accent
instead of default blue.
- Light + dark themes via prefers-color-scheme; muted text now meets WCAG AA
(replaces low-contrast #aaa/#888).
- Macro cards read as physical keys (raised, hairline highlight, per-macro
--macro-color wash + placeholder, press/executing feedback, 2-line clamp).
- Proper modal sheet (blurred scrim, sticky header/footer, focus rings),
thumb-reachable floating action button, 44px tap targets, app-wide
:focus-visible, and prefers-reduced-motion support.
- manifest + theme-color meta updated to the new palette.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
frame-ancestors is ignored inside a <meta> CSP, so move clickjacking
protection to real response headers (X-Frame-Options: DENY) and add
X-Content-Type-Options: nosniff and Referrer-Policy: no-referrer on all
responses. Drop the ineffective frame-ancestors token from the meta CSP.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Update README for the new security model and features: local API access
token embedded in the URL/QR (and web.allow_lan), app commands executing
without a shell, relay password in OS keyring, web-based macro creation,
and media/system keys.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses two Major findings from PR review:
- Auth lockout no longer griefable: the throttle key is namespaced per path
and includes the client IP (web:<session>:<ip> vs desktop:<session>:<ip>),
so a flood of bad web-client auths can neither lock other clients of the
same session nor block the desktop from (re)authenticating. Per-socket
failure close and brute-force lockout are preserved.
- Relay web client XSS eliminated: app.html tab/macro rendering rebuilt with
createElement/textContent (no macro/tab value reaches innerHTML); inline
scripts/handlers externalized to /static/app.js and /static/login.js so the
helmet CSP now uses script-src 'self' (dropped 'unsafe-inline' for scripts).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remote (relay) devices browse and trigger macros; macro authoring is
intentionally kept to the local network so what runs on the machine isn't
editable from the public internet.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Minor PR-review follow-ups for the local PWA:
- Macro images load via a header-authenticated fetch -> blob object URL in both
token and relay modes, so no credential ever sits in an image URL; prior
object URLs are revoked on re-render.
- Local-mode auth failure now shows a clear "Access token required" state and
stops the reconnect/toast loop (one-shot), instead of retrying forever.
- CSP connect-src tightened from 'self' ws: wss: to 'self' (same-origin WS is
covered); verified the live WebSocket still connects.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Uniform auth responses remove the session-enumeration oracle: unknown
session and wrong password now return an identical 401 at the API layer and
an identical WS handshake/close; desktop-status and the 503 "not connected"
signal are only exposed after successful auth.
- Session-count cap re-checked after bcrypt.hash so concurrent creates can't
overshoot maxSessions.
- Relay web client reconnect backoff resets only after a successful auth
response (an accept-then-close server no longer defeats the backoff).
- idGenerator comment corrected to match the rejection-sampling; drop unused
recordFailure return value.
- DEPLOY.md: document ALLOWED_ORIGINS for reverse-proxy deployments.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
jknapp
merged commit e9b3108226 into main2026-07-18 04:08:24 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Closes the unauthenticated remote-code-execution chain found in a security audit and continues through the rest of the audit's findings, adding web-based macro authoring and a visual redesign. 7 commits, grouped by concern.
Security
/api/*routes and the/wshandshake (timing-safe compare); PWA shell stays public to bootstrap. Macroappcommands no longer run through a shell (shell=Trueremoved;shlex-tokenized, exec'd directly) — kills;/&&/|/$()/redirection injection. LAN binding is configurable (web.allow_lan). Image path-traversal in/api/imagefixed. Anti-clickjacking response headers./healthno longer leaks session IDs; credentials removed from URLs (header-based); sessions bounded + TTL-pruned; 12-char session IDs with rejection sampling; atomic/debounced session store; helmet CSP, trust proxy, restricted CORS; sane.env.example.innerHTML), strict CSP, no inline handlers,JSON.parseguarded.Robustness
macros.jsonwrites, execution moved outside the lock, debouncedlast_used, key/wait validation./api/executeoffloaded off the event loop, image upload hardened (size cap, Pillow validation, server-generated name, relative path only).minimize_to_trayhonored.Features & design
Verification
End-to-end against the real FastAPI server in a headless browser: token auth (401/200), WebSocket connect, macro created through the web editor (correct schema, atomic persist, live refresh), and the shell-injection RCE confirmed closed (a redirection payload did not execute). Relay TypeScript type-checks clean; Python compiles; concurrent macro-store writes verified deadlock-free.
Review round (included in this branch)
An automated completeness review of the branch confirmed all intended changes are present and the P0 RCE chain is genuinely closed, and caught two Major issues that were then fixed here:
sessionIdalone and shared between the web/desktop paths, letting anyone with the share link lock out the owner and block the desktop. Now namespaced per-path and per-IP.macropad-relay/public/app.html. Now rebuilt with safe DOM construction, externalized scripts, andscript-src 'self'.All minor findings were also addressed (credentials out of local image URLs, local auth-failure state, tightened
connect-src, relay enumeration-oracle removal, atomic session cap, backoff-after-auth,ALLOWED_ORIGINSdeploy note). The relay web client is intentionally kept execute-only (documented in the README).Not run interactively
The PySide6 desktop GUI and the live relay were reviewed + compile/type-checked but not exercised at runtime (no desktop session / relay deployment here) — worth a manual smoke test.
🤖 Generated with Claude Code
The local FastAPI server previously exposed macro create/execute over the network with no authentication while bound to 0.0.0.0, and macro `app` commands ran via subprocess with shell=True — an unauthenticated RCE for anyone on the LAN (and, via the relay, the internet). - Mandatory per-install auth token (secrets.token_urlsafe) required on all /api/* routes and the /ws handshake; timing-safe hmac.compare_digest. PWA shell + /static assets remain public to bootstrap. - Token auto-generated and persisted in settings.json (now chmod 0600). - Bind host is configurable (web.allow_lan); token is the security boundary. - macro `app` launch no longer uses shell=True — commands are tokenized with shlex and exec'd directly, killing ;/&&/|/$() shell chaining. - Relay client injects the local token and only forwards /api/* paths, and no longer forwards attacker-supplied headers to the local server. - Fixed path traversal in /api/image/{path} (realpath-confined to macro_images). - GUI embeds the token in the LAN URL/QR; PWA reads it and sends it on API, WebSocket, and image requests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Security: - Eliminate DOM-based XSS: renderMacros/renderTabs rebuilt with createElement/textContent/setAttribute + addEventListener. No macro name, id, image_path, or category ever reaches innerHTML. - Remove all inline on*= handlers; add a strict Content-Security-Policy meta (script-src 'self', object-src 'none', base-uri 'none', frame-ancestors 'none'). - Guard JSON.parse on WebSocket frames. Robustness: - Service worker: network-first for the app shell + JS/CSS so fixes actually ship; synthetic 503 for offline API calls; drop no-op /ws intercept; bump cache version. - WebSocket reconnect: exponential backoff (1s→30s), tear down old socket before reconnect, stop on pagehide/beforeunload. - Tab-switch race fixed with a request-id guard; executing state cleared in finally. Accessibility: - Macro cards and tabs are real buttons with aria-labels/roles; toast + connection status are aria-live; removed user-scalable=no. Feature — web-based macro creation/editing: - Floating "+" button + modal command-sequence builder (text/key/hotkey/ wait/app) with reorder/remove; create via POST, edit via PUT (prefilled from GET /api/macro/{id}), delete with a named confirm. Works in both local and relay auth modes. Empty state now offers "Create your first macro". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>