Please report suspected vulnerabilities privately — do not open a public issue.
Use GitHub's Private vulnerability reporting (the "Report a vulnerability" button on the Security tab), or email jason@superdesign.dev.
We aim to acknowledge a report within a few business days, agree on a disclosure timeline, and credit reporters who wish to be named.
tools-registry is built so that reading the source does not help an attacker — nothing load-bearing is hidden in the code. Secrets live in the database (encrypted with a Fernet key held only in the server environment) and enforcement happens server-side and in the operating system.
- The proxy never hands the key to the caller. For an HTTP tool, the registry injects the credential server-side and makes the upstream call; the consumer's token only authorizes it.
- Encryption at rest. Stored secrets are Fernet-encrypted; the key is an environment variable, never in the repo or the database.
- Tenant isolation. Every secret, tool, and record is scoped to an org; access is gated by role and, per member, by an explicit tool allow-list.
- SSRF guard. A tool's upstream host is re-resolved at call time and internal/metadata addresses are refused (defeats DNS-rebinding).
- Local runs are sandboxed.
treg runon a member's machine executes the CLI as a locked-downtreg-runuser, with an egress allow-list and an optional filesystem jail, so a shared key can't be read, exfiltrated over the network, or written to a member-readable file. - Server runs are resource-limited.
treg run --serverexecutes each CLI with a scrubbed environment (treg's own secrets removed), a per-run throwaway home, an allow-list of runnable commands, output redaction, and POSIX resource limits (CPU, file size, no core dumps). - Signup credit belongs to a verified account, once. Only an email OTP, provider-verified
Google/GitHub email, or inbox-only invitation link marks an identity verified. Legacy
POST /usersand admin-visible invitation codes do not.claim_signup_promoatomically consumes the user's eligibility in the same transaction as the money grant; team deletion never restores it. Accounts predating revision0033receive no new automatic grant; their existing balances are unchanged. New identities can still be farmed through verified inboxes, so this is not a one-human guarantee.TREG_PROMO_GRANT_MICRO=0stops new automatic grants after deployment. The domain blocklist (TREG_BLOCKED_EMAIL_DOMAINS, unset means no blocks) remains a configurable speed bump at every sign-in/sign-up door and both team-creating endpoints; it fails open on classifier errors. Suspend abusive users and teams separately, retaining their records for investigation.
R2 credentials are server environment settings (TREG_ARCHIVE_OBJECT_STORE_ACCESS_KEY_ID and
TREG_ARCHIVE_OBJECT_STORE_SECRET_ACCESS_KEY), never catalog credentials or caller input. Use a token scoped
to the private archive bucket with object read/write access; bucket administration is unnecessary.
The configured endpoint must be an HTTPS Cloudflare R2 account endpoint. Object names are always
SHA-256 hashes computed by treg; clients cannot supply an object path, bucket, host or URL.
The obstore compiled wheel exists only in the server extra and is imported lazily by the
server factory, initialized by bootstrap. Its shared Rust HTTP client uses configured connect and
request timeouts and zero SDK retries; archive_bodies owns retry policy. SDK exceptions, signed
requests, credentials and response bodies are not included in archive telemetry.
obstore S3Store uses checksum_algorithm=SHA256 and single-part PUT. R2 validates the transmitted
SHA-256 checksum before PUT succeeds; only then can a DB pointer be committed. PUT performs no
follow-up HEAD. HEAD makes one request for object size. No custom digest metadata is written or
required. GET enforces the size limit and checks downloaded bytes against the object's SHA-256
hash. The bucket is not public and no object URL is exposed: call-history authorization checks
the team's CallRecord before resolving its archive reference. A hash is an identity, not an
access credential. Upstream answers are retained under the existing archive policy. A provider
that echoes treg's PLATFORM credential in a 2xx body is not sanitized by these guards (judge that
provider's cache with it in mind); an answer that echoes a team's OWN credential is refused by
the recorder (_echoes_own_credential). An own-credential answer is stored under a cache key that
folds in the org (or the connection, on an own_account endpoint), so another team never computes
the hash that would find it; only an endpoint-level cache.sharing: public declaration puts such an
answer on the public key.
Object I/O never holds the calling task's database connection. Startup refuses incomplete R2
configuration when archive mode is enabled and any storage switch selects R2. The manual smoke
script accepts only treg-archive-dev, refuses CI, and skips absent credentials. It creates no
cloud resources apart from one small test object. Production configuration belongs in the private ops project.
Honesty is part of the model. Two items are deliberately deferred:
- Server runs do not yet have filesystem/network isolation. The resource limits above cap denial of service, but a full jail (a locked-down user + egress allow-list, like the local sandbox) requires a container deployment and is planned. In the supported hosted configuration the encryption key is an environment variable rather than an on-disk secret file, and only allow-listed CLIs may run.
- The CLI-login handshake is in-process. The short-lived pairing state for
treg loginlives in the server process (it self-heals on retry and carries no rate-limit value). Running more than one server instance requires sticky routing for that one flow, or moving it to shared storage.
This project is pre-1.0; security fixes land on main. Pin a commit if you need stability.