Security model
A short statement of what the Phantom REST API protects against, what it doesn't, and what your responsibilities as an integrator look like.
What the API protects you against
Token leakage (partial)
- Tokens are stored only as SHA-256 hashes — a database read can't yield a usable token.
- Tokens carry an unmistakable
phk_live_prefix so a token committed to a public repo is recognisable to GitHub's secret scanner. - Tokens are shown to the operator exactly once at create time.
- Revocation is immediate. The lookup cache TTL is 60 seconds, and
revokebusts the cache the moment it runs. - Per-token IP allowlist lets you pin a token to specific addresses or CIDRs.
What this doesn't protect against is a token leaking from your system to an attacker who is on your allowlist. Treat your tokens like passwords.
Replay attacks
- Webhook deliveries include a unix timestamp in the signed header and we reject anything more than 300 seconds out of date.
- API writes can be made idempotent with the
Idempotency-Keyheader so a network mid-flight failure can be retried without double-execution.
SSRF (server-side request forgery) via outbound webhooks
A malicious endpoint URL that resolves to private infrastructure is a real threat. We mitigate it with two passes:
- At create time, the URL's host is parsed, validated against a hostname blocklist, and DNS-resolved; any address falling inside a blocked CIDR (loopback, RFC1918, link-local, carrier-grade NAT, cloud metadata endpoints) is refused.
- At delivery time, we re-resolve. This defeats DNS rebinding: an endpoint that passed step 1 but flips DNS to
127.0.0.1before delivery is still blocked.
Additionally, the HTTP client used for delivery is configured with allow_redirects=false. A redirect-aware delivery could be exploited to bounce a payload to a private address via a Location header.
Rate-limit-based DoS
- Per-token read and write buckets so a runaway integration on one token doesn't starve your other integrations.
- Per-IP unauthenticated throttle so a probing attacker can't burn through token-prefix lookups for free.
Cross-customer leaks
- Every API call is scoped to the token's guild_id at the auth boundary — there is no path in
/api/v1/*that takes aguild_idparameter. Resource ids resolved without the token's guild filter (path-only{id}params) are joined toguild_idat the controller level, so a guess of another customer's case id returns404, not their data.
Audit + abuse detection
- Every API call is logged with the token id, HTTP method, route, status code, source IP and latency.
- Logs are kept for 14 days by default — long enough for investigation, short enough to keep the privacy footprint small.
- Failing responses (
4xx/5xx) are indexed separately so abuse-detection lookups stay fast.
What we can't protect against
A leaked token, used from an allowed IP
If your CI server's secrets get exfiltrated and the attacker calls the API from your CI server, we have no way to tell. Defensive practice: minimum scopes, short expiry on CI tokens, and last_used_at review on the dashboard.
A buggy receiver that exposes its endpoint URL
Your webhook URL isn't a secret — it's reachable by anyone who knows it. Always verify the Phantom-Signature header before trusting any field in the payload. If your handler honours payload fields before verifying, an attacker who knows your URL can spoof any event.
Receiver-side replay of unsigned data
Even with the signature check, you should dedupe on id server-side. We retry deliveries on transient failure; if your handler took 11 seconds to ack the first attempt and we already re-queued, the second attempt will arrive with the same id. Dedupe = no double-execution.
Logical authorisation inside your service
A token with economy:write can adjust any user's balance in the bound guild, not just specific users. We have no way to know your application's notion of "who should be able to change whose balance." Enforce that yourself on top of what the API gives you.
Compliance points
- No PII in URLs. Discord user ids are technically pseudonymous, but they are still personally identifying. We accept them in path parameters and request bodies but request logs only record the route template (
/cases/{id}) — never the resolved id. - TLS-only. Plain HTTP requests are rejected. Outbound webhook delivery refuses
http://URLs entirely. - Encryption at rest. API token plaintext is never persisted (we only ever store a SHA-256 hash; you see the token once at creation). Webhook signing secrets are encrypted at rest with AES-256-GCM.
What to monitor on your side
last_used_atfor every token — a token that hasn't fired in 90 days probably doesn't need to exist.- 4xx rate on your integration — a sudden spike in
403 insufficient_scopeusually means a recent scope edit dropped something the integration relied on. - Auto-disabled webhooks — surface these to your on-call rota the same as any other dependency outage.
Reporting a vulnerability
If you believe you've found a security issue in the API or the webhook delivery surface, please email security@phantombot.gg — do not file a public GitHub issue. We treat reports seriously and will get back to you within one business day.
