feat(security): tighten ingest trust boundary and add invariant tests

- Remove CORS headers and OPTIONS preflight from daemon
- CORS was enabling cross-origin browser POSTs to loopback endpoint
- No legitimate consumer is a browser (emitter uses curl, TUI uses Go http)

Add invariant test script test-invariants.sh:
- Daemon listens on 127.0.0.1 only (loopback-only binding)
- No synthesized input: verify no tmux send-keys in daemon sources
- OPTIONS preflight is no longer honored (returns 404)

Validates trust boundary specified in docs/plan/plan.md section
'Failure modes & invariants'.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-07-02 09:03:55 -04:00
parent cab4e37537
commit 116464fe30
2 changed files with 137 additions and 11 deletions

View file

@ -27,17 +27,6 @@ setInterval(() => cleanupQueue(), 60 * 60 * 1000);
const server = http.createServer(async (req, res) => {
const url = new URL(req.url || "", `http://${req.headers.host}`);
// CORS for local testing
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type, X-Tmux-Pane");
if (req.method === "OPTIONS") {
res.writeHead(204);
res.end();
return;
}
try {
// POST /event - hook ingest endpoint
if (req.method === "POST" && url.pathname === "/event") {

137
test-invariants.sh Executable file
View file

@ -0,0 +1,137 @@
#!/bin/bash
# Invariant tests for Trail Boss daemon
# Tests the trust boundary and input guarantees specified in docs/plan/plan.md
set -e
TB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DAEMON_URL="http://127.0.0.1:4000"
DATA_DIR="$HOME/.local/share/trailboss"
DAEMON_PID=""
# Cleanup function
cleanup() {
echo "[cleanup] stopping daemon..."
[ -n "$DAEMON_PID" ] && kill "$DAEMON_PID" 2>/dev/null || true
rm -rf "$DATA_DIR" 2>/dev/null || true
}
trap cleanup EXIT
echo "=== Invariant Tests ==="
echo "Testing trust boundary and input guarantees"
echo ""
# Clean slate
cleanup
mkdir -p "$DATA_DIR"
# ========================================================================
# Invariant 1: Daemon binds loopback only (127.0.0.1)
# ========================================================================
echo "=== Invariant 1: Loopback-only binding ==="
# Start daemon and capture its log output
cd "$TB_DIR/daemon"
bun index.ts > /tmp/trailboss-daemon-log-$$ 2>&1 &
DAEMON_PID=$!
sleep 2
# Check the log for the binding address
if grep -q "listening on http://127.0.0.1:4000" /tmp/trailboss-daemon-log-$$; then
echo "[ok] Daemon bound to 127.0.0.1:4000 (loopback only)"
else
echo "[fail] Daemon did not log loopback binding"
cat /tmp/trailboss-daemon-log-$$
exit 1
fi
# Verify daemon is reachable on loopback
if curl -s --max-time 1 "$DAEMON_URL/status" >/dev/null 2>&1; then
echo "[ok] Daemon is reachable on loopback (127.0.0.1)"
else
echo "[fail] Daemon not reachable on loopback"
exit 1
fi
# Verify daemon is NOT reachable on a non-loopback interface
# Get the first non-loopback IP address (if any)
NON_LOOPBACK_IP=$(ip -4 addr show 2>/dev/null | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | grep -v '^127\.' | head -1 || echo "")
if [ -n "$NON_LOOPBACK_IP" ]; then
# Try to connect on the non-loopback interface - should fail
if curl -s --max-time 1 "http://$NON_LOOPBACK_IP:4000/status" >/dev/null 2>&1; then
echo "[fail] Daemon is reachable on non-loopback interface $NON_LOOPBACK_IP"
exit 1
else
echo "[ok] Daemon is NOT reachable on non-loopback interface $NON_LOOPBACK_IP"
fi
else
echo "[skip] No non-loopback interface available for testing"
fi
rm -f /tmp/trailboss-daemon-log-$$
# ========================================================================
# Invariant 2: No synthesized input (no tmux send-keys in daemon sources)
# ========================================================================
echo ""
echo "=== Invariant 2: No synthesized input ==="
# Check all daemon TypeScript sources for tmux send-keys invocations
DAEMON_SOURCES=$(find "$TB_DIR/daemon" -name "*.ts" -type f)
SEND_KEYS_COUNT=$(grep -r "send-keys" $DAEMON_SOURCES 2>/dev/null | wc -l)
if [ "$SEND_KEYS_COUNT" -eq 0 ]; then
echo "[ok] No 'send-keys' found in daemon source files"
else
echo "[fail] Found $SEND_KEYS_COUNT occurrences of 'send-keys' in daemon sources:"
grep -r "send-keys" $DAEMON_SOURCES 2>/dev/null
exit 1
fi
# Also check for the more explicit "tmux.*send-keys" pattern
TMUX_SEND_KEYS_COUNT=$(grep -rE "tmux.*send-keys|send-keys.*tmux" $DAEMON_SOURCES 2>/dev/null | wc -l)
if [ "$TMUX_SEND_KEYS_COUNT" -eq 0 ]; then
echo "[ok] No 'tmux send-keys' patterns found in daemon source files"
else
echo "[fail] Found $TMUX_SEND_KEYS_COUNT occurrences of tmux/send-keys patterns:"
grep -rE "tmux.*send-keys|send-keys.*tmux" $DAEMON_SOURCES 2>/dev/null
exit 1
fi
# ========================================================================
# Invariant 3: OPTIONS preflight is no longer honored (trust boundary)
# ========================================================================
echo ""
echo "=== Invariant 3: OPTIONS preflight rejected ==="
# Send an OPTIONS preflight request (what a browser would send for CORS)
OPTIONS_STATUS=$(curl -s -o /dev/null -w "%{http_code}" -X OPTIONS "$DAEMON_URL/event" \
-H "Origin: http://evil.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: X-Tmux-Pane")
if [ "$OPTIONS_STATUS" = "404" ]; then
echo "[ok] OPTIONS preflight returns 404 (CORS not supported)"
elif [ "$OPTIONS_STATUS" = "204" ]; then
echo "[fail] OPTIONS preflight returns 204 (CORS is still enabled - trust boundary violated)"
exit 1
elif [ "$OPTIONS_STATUS" = "200" ]; then
echo "[fail] OPTIONS preflight returns 200 (unusual response)"
exit 1
else
echo "[ok] OPTIONS preflight returns $OPTIONS_STATUS (not 204, CORS not supported)"
fi
# ========================================================================
# Summary
# ========================================================================
echo ""
echo "=== All Invariant Tests Passed ==="
echo ""
echo "✓ Invariant 1: Daemon binds loopback only"
echo "✓ Invariant 2: No synthesized input (no send-keys in daemon)"
echo "✓ Invariant 3: OPTIONS preflight rejected (CORS disabled)"
echo ""
echo "[ok] Trust boundary and input guarantees verified"