SPB Git

spb/autoscholar

Public
15.2 KB

# Security — AutoScholar.ai

Status: living document, updated per phase. Everything listed under Enforced is implemented in code and covered by tests named in docs/TEST_REPORT.md; items under Limitations are honest gaps.

# Threat model summary

# Threat (from the build spec §11) Enforcement Evidence
1 Credential attacks on the single admin account, incl. admin123 after rotation Argon2id (m=64 MiB, t=3, p=4); bootstrap password used only while admin_users is empty, flagged must_change_password; rotation revokes all sessions and rotates the token; old hashes kept in password_changes and rejected; per-IP + per-user rate limits (20/15 min, 10/15 min) and exponential lockout from 5 failures (1 min → 1 h); bootstrap password reuse refused by policy integration test "bootstraps admin123 once, forces rotation, rejects reuse", "locks out after repeated failures"; e2e admin login tests
2 Broken access control to admin/internal APIs, logs, unpublished papers Every /api/admin/* handler calls requireAdmin() (server-side session lookup, idle/absolute expiry, CSRF on mutations, same-origin check, mutation rate limit); /api/internal/* rejects requests carrying tunnel headers (X-Forwarded-*), enforces the private CIDR allow-list and the worker bearer token (SHA-256 stored, asw_ prefix for redaction); public queries only read papers/paper_versions projections, never run/worker tables; private artifacts only through the admin route e2e: every admin route returns 401 signed-out; integration: worker auth/disable/quarantine
3 Malicious Markdown / XSS in uploaded CLAUDE.md and metadata Upload accepts UTF-8 .md/.txt ≤ 256 KiB, rejects control chars, path separators, platform-targeting patterns; original bytes + SHA-256 stored; preview rendered server-side with marked + sanitize-html strict allowlist; CSP (default-src 'self', no inline scripts except Next runtime) unit tests validateInstruction; e2e XSS preview test
4 Prompt injection from web pages, PDFs, datasets, repositories Platform policy lives in the SDK options (hooks, disallowedTools, sandbox), not in prompts; fetched content cannot add tools or permissions; PreToolUse hook denies LAN/loopback/IP-literal/cluster-name URLs for WebFetch and MCP tools (DNS-resolved SSRF guard); reviewer session is write-restricted to review/ selftest (autoscholar-worker selftest), Phase 5 malicious-instruction run
5 Agent access to cluster admin, SSH keys, password stores, other workspaces, production source Worker runs as dedicated non-admin hidden user asworker under a LaunchDaemon; workspaces /var/autoscholar/runs/<run-id> (0750 root dir); SDK sandbox (Seatbelt): writes limited to the workspace, denyRead on /Users, /etc/autoscholar, keychains, sudoers; hook denies sudo/ssh/launchctl/pfctl/dscl/pm2/brew/..., paths under /Users, ~/.ssh, other run directories; disallowedTools scoped Bash(sudo *) etc. apply in every mode; env of the SDK subprocess is replaced (only PATH, isolated HOME/CLAUDE_CONFIG_DIR, API key); no SSH keys on the worker user; platform source lives in /var/autoscholar/bundle owned by asworker but not writable from the sandbox (outside allowWrite) selftest + Phase 5 denial log
6 SSRF, path traversal, symlink escape, sandbox escape, shell injection, unsafe archives Storage keys normalised and root-checked (assertSafeKey); artifact descriptor rejects ../absolute paths; hook resolves real paths (symlink-safe) for Write/Edit; Seatbelt profile for the platform-side reproduction step; no shell interpolation of uploaded text anywhere (uploaded Markdown only ever becomes a file on disk) storage unit tests; policy hook
7 Overly broad worker access to storage/DB Workers never see the DB or the artifact store: only the HTTP protocol with lease-scoped fencing tokens; uploads are server-hashed and stored under server-generated keys integration fencing tests
8 Queue duplication, stale lease completion, cancellation races, unauthorized publish decisions Idempotency key unique index; SELECT … FOR UPDATE SKIP LOCKED leasing; monotonic fencing token per run checked on every worker call; lease expiry → retry with checkpoint; publish decision computed by services/qa on the server from artifacts, never from model text; publication idempotent by paper_versions.run_id unique index and sequence allocation inside the transaction integration: idempotent launch (3 concurrent), fencing, crash/expiry, cancel race, publication concurrency
9 Secret exfiltration via logs, outputs, PDFs, citations, telemetry redact() applied to all log lines and all run events before persistence; QA hard gate secrets.none scans text artifacts for key patterns; input/, provenance/events.jsonl, .env, key files are never promoted to public; SDK subprocess env contains no worker token; selftest asserts worker.env never appears in output integration: events contain no sk-ant-; gate unit coverage
10 Resource exhaustion, uncontrolled bills, infinite retries, oversized artifacts Manifest caps maxBudgetUsd (≤ 2000), maxRuntimeMinutes, maxAttempts (≤ 5) enforced by the scheduler and the runner (per-stage maxBudgetUsd/maxTurns/wall-clock abort); scheduler cancels runs over the runtime cap; artifact ≤ 512 MiB (ingest) / 256 MiB (runner), instruction ≤ 256 KiB, JSON bodies bounded; attempts stop at maxAttempts integration: runtime cap, attempt limit
11 Accidental public release of private/PII/copyrighted material Promotion allow-list (PUBLIC_PROMOTION_RULES) + deny-list; synthetic artifacts can never be published; retraction and artifact withdrawal (HTTP 410) keep tombstones; literature connectors store metadata + links, not full texts integration publication test checks input/CLAUDE.md and code/.env absent from the public bundle
12 Supply chain, TLS, backups pnpm lockfile, pinned ranges; pnpm audit in CI; TLS by Caddy on the MacLustr gateway; HSTS in production; nightly backups (docs/OPERATIONS.md) Phase 8

# Execution isolation on macOS workers (what is and is not a boundary)

  • Is a boundary: separate POSIX user (asworker, non-admin, hidden), Seatbelt sandbox for every Bash/tool command (write scope = workspace, read denials on sensitive paths, hostname-allowlisted egress proxy, no local binding), pf rules per uid blocking RFC1918/CGNAT/link-local egress except the control plane, platform hooks (deny beats everything), replaced process environment.
  • Is not a boundary by itself: the working directory. We do not claim it is. The Seatbelt profile and the user separation are the boundaries; the hook is defense in depth.
  • Not available today: Linux containers/VMs (no Docker daemon running on any node). If stronger isolation is required (e.g., untrusted binary workloads), block dispatch until a container host exists; the worker refuses to run when the sandbox is unavailable (failIfUnavailable: true, sandboxAvailable=false ⇒ never leased).

# Session and transport

  • Cookie as_session: HttpOnly, Secure (production/https), SameSite=Strict, 12 h absolute, 2 h idle; opaque random token, SHA-256 stored; CSRF = HMAC(session secret, token hash) echoed in x-csrf-token.
  • Admin and public traffic: HTTPS via Caddy (automatic certificates) on the MacLustr gateway → WireGuard → node. Internal worker traffic: LAN only, bearer token; not reachable through the gateway (header check) and limited to INTERNAL_API_ALLOWED_CIDRS.
  • Security headers: CSP, X-Frame-Options SAMEORIGIN, nosniff, Referrer-Policy, Permissions-Policy, HSTS (prod).

# Known limitations / follow-ups

  • No mTLS between worker and control plane (no PKI); tokens are revocable and scoped. Rotation endpoint: re-enroll with the enrollment secret.
  • The SDK sandbox proxy is hostname-based and does not inspect TLS; allowlisted hosts are trusted at the hostname level.
  • MFA (TOTP/passkey) is scaffolded in the schema (totp_secret) but not enabled in v1.
  • The clientIp used for limits relies on X-Forwarded-For only when TRUST_PROXY=1 (behind Caddy); direct LAN requests carry no IP (Next.js does not expose the socket), so per-IP limits apply per deployment edge.
Files · e709d44366a0d3dc4c8b6b32ba43ac31859be02a
Menu

Theme