Shared Secret not enforced on the Command-Line / Unix-socket transport (6.640)

  • Type of Mac: MacBook Pro M2 Max
  • macOS version: 26.5.2
  • BetterTouchTool version: 6.640 (build 2026071312)

Product: BetterTouchTool 6.640 (build 2026071312)
macOS: 26.x, Apple Silicon
Severity: Security — the scripting shared secret is bypassable on one transport

Summary

When a Shared Secret For Scripting is configured, the secret is correctly enforced on the AppleScript transport but is completely ignored on the Command-Line / Socket Server transport (/tmp/com.hegenberg.BetterTouchTool.sock, used by bttcli). Any local process can issue read and write scripting commands over the socket with no secret, or a wrong secret, and they succeed.

Because the socket accepts action-executing commands (e.g. shell-command actions), this effectively means: any process that can reach the socket path can run arbitrary commands / read and rewrite the full trigger config, regardless of the shared secret. The secret gives a false sense that scripting is gated.

Environment

  • Shared secret is set (Scripting Settings → "Shared Secret For Scripting"); confirmed saved to the login keychain as BTTScriptingSharedSecret.
  • "Allow external BetterTouchTool Scripting" ON; "BTT Socket Server For Command Line Usage" ON.
  • Reproduced on two separate machines, both on 6.640, and it persists across a full BTT restart (verified new PID) on both.

Steps to reproduce

  1. Set a Shared Secret For Scripting; enable the Command-Line / Socket Server.
  2. Send a request to the socket with no shared_secret, and again with a wrong one. The socket speaks a bare request line: /<command>/?param=value…. Minimal Python:
import socket
def req(line):
    s = socket.socket(socket.AF_UNIX); s.connect("/tmp/com.hegenberg.BetterTouchTool.sock")
    s.sendall((line + "\r\n\r\n").encode())
    out = b""
    while True:
        b = s.recv(65536)
        if not b: break
        out += b
    return out.decode(errors="replace")

print(req("/get_string_variable/?variableName=SOME_VAR"))                 # no secret
print(req("/get_string_variable/?variableName=SOME_VAR&shared_secret=WRONG"))  # wrong secret

Actual result

Both calls succeed and return the real value. get_triggers, add_new_trigger, update_trigger, delete_trigger, trigger_named etc. all work over the socket with no/incorrect secret. The shared_secret parameter appears to be parsed-but-ignored on this transport.

Expected result

With a shared secret configured, a socket request lacking the correct shared_secret should be rejected (as the AppleScript transport does — a secretless AppleScript call errors/does not return data, and a wrong secret yields missing value). Enforcement should be consistent across all scripting transports (URL scheme, AppleScript, webserver, and command-line/socket).

Contrast — AppleScript enforces correctly (same machine, same secret)

  • No secret → command hangs then AppleEvent timed out (-1712).
  • Wrong secret → missing value.
  • Correct secret → returns the value.

Only the socket/CLI transport is unauthenticated.

Impact

The socket is a Unix domain socket in world-traversable /tmp (mode srw-rw-rw-). Users who enable the shared secret specifically to gate scripting are not actually gated on this transport. Since socket reachability == arbitrary action execution, this is a local privilege/persistence surface that the secret is expected to close.

Suggested fix

Apply the same shared_secret check the other transports use to the socket request handler; reject on missing/mismatched secret before dispatching the command.

Thanks a lot for reporting! Will be fixed with the next build later today, there was indeed a logic issue that caused the shared secret to not be populated for the cli support after enabling that.