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 inx-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-Foronly whenTRUST_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.