Current state: API keys (arch_*) are stored in plaintext in the Agent.apiKey field.
Risk: If the database is compromised, all API keys are exposed immediately.
Migration plan (safe, zero-downtime):
-
Schema change — Add a nullable hash column to Prisma schema:
model Agent { // ... existing fields ... apiKeyHash String? @unique // nullable during migration period }
-
Deploy new schema — Run
npx prisma migrate dev --name add-api-key-hash. -
Migration script — Hash all existing keys and populate
apiKeyHash:import crypto from "crypto"; import { prisma } from "./src/lib/prisma"; async function migrateKeys() { const agents = await prisma.agent.findMany({ where: { apiKeyHash: null } }); for (const agent of agents) { const hash = crypto.createHash("sha256").update(agent.apiKey).digest("hex"); await prisma.agent.update({ where: { id: agent.id }, data: { apiKeyHash: hash } }); } console.log(`Migrated ${agents.length} agents`); } migrateKeys();
-
Auth middleware — Update to try hash lookup first, fall back to plaintext for backward compatibility, then hash-and-save on plaintext hit:
const hash = crypto.createHash("sha256").update(apiKey).digest("hex"); agent = await prisma.agent.findUnique({ where: { apiKeyHash: hash } }); if (!agent) { // Plaintext fallback (migration period only) agent = await prisma.agent.findUnique({ where: { apiKey } }); if (agent) { // Opportunistically migrate this key await prisma.agent.update({ where: { id: agent.id }, data: { apiKeyHash: hash } }); } }
-
After migration window — Once all keys have
apiKeyHashpopulated, drop the plaintextapiKeycolumn and remove the fallback lookup.
Why not done now: Existing users' plaintext keys cannot be recovered post-hashing. A migration window is required so the transition is seamless.
ADMIN_KEY defaults to "changeme" in development. A startup guard in api/src/index.ts
prevents the server from starting in production with this default (or with ADMIN_KEY unset).
Action required: Set ADMIN_KEY to a strong random value in your Render environment variables.
CORS_ORIGIN defaults to "https://archtools.dev". Override via the CORS_ORIGIN environment
variable if you need to allow additional origins.
All tool endpoints that accept user-supplied URLs validate them through api/src/lib/ssrf.ts
before making outbound HTTP requests. This blocks:
- Private IP ranges (RFC 1918, link-local, loopback)
- Cloud metadata endpoints (169.254.169.254, metadata.google.internal, etc.)
- Render internal ranges (100.64.x.x)
- DNS rebinding (IPs are resolved and checked post-DNS-lookup)
The OAuth /token endpoint uses crypto.timingSafeEqual to compare client_secret values,
preventing timing-based secret enumeration attacks.
Please email security issues to the repository owner. Do not open public GitHub issues for vulnerabilities.