- 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
- Set a Shared Secret For Scripting; enable the Command-Line / Socket Server.
- 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.