# Discord PHP Bot Security

> Audit checklist for DiscordPHP bots and API libraries — stop the bot token leaking to third-party APIs or logs, keep secrets out of custom_ids and exception messages, use constant-time comparison and crypto-random for auth/CSRF/webhook flows, and don't log OAuth codes / full headers / PII. Use when reviewing an HTTP client, an error handler, an OAuth or webhook endpoint, or before publishing a repo.

- Skill: `discord-php/discord-php-bot-security` (Agent Skill)
- Install (CLI): `npx skillmds@latest add discord-php/discord-php-bot-security`
- Raw SKILL.md: https://api.skillmd.com/api/skills/discord-php/discord-php-bot-security/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: discord-php (https://skillmd.com/u/discord-php)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/discord-php/discord-php-bot-security

---


# DiscordPHP Bot Security Skill

A concrete checklist built from real findings across `DiscordPHP-MTG`,
`DiscordPHP-NHA`, `DiscordPHP-Voice` and `Civilizationbot`.

## 1. Never send the Discord bot token to a third party

The classic bug: an extension's `Http` client for `api.example.com` is
constructed with `'Bot '.$this->token` and adds `Authorization: <token>` to
**every** request — so the Discord bot token is sent to an unrelated host on
every call.

- Extension HTTP clients for a third-party API should take an **empty** token and
  only set `Authorization` when a real value is present:
  ```php
  $h = ['User-Agent' => $this->getUserAgent()];
  if ($this->token !== '')  $h['Authorization'] = $this->token;
  if ($this->apiKey !== null) $h['X-Api-Key'] = $this->apiKey;   // that API's own key
  ```
- Drop Discord-only headers (`X-Ratelimit-Precision`) from non-Discord clients.
- Override `getUserAgent()` to identify your library, not `DiscordPHP-HTTP`.

## 2. Keep tokens out of logs and exception messages

- Do not log full request/response bodies on error. A FastAPI-style **422** body
  echoes the request back under `input` — for an auth'd POST that includes the
  token. Redact before logging *and* before putting it in an exception message:
  ```php
  $body = preg_replace('/("(?:nha_)?token"\s*:\s*)"[^"]*"/i', '$1"***"', $body) ?? $body;
  ```
- `Manager`/gateway code: log `['token' => '*****']`, and give payload parts a
  `__debugInfo()` that redacts the token (see `VoicePayload::__debugInfo()`).
  Log identify with `['op' => $payload->op]`, not the whole payload.
- Global error handlers that DM a technician: send `file:line:function` from
  `debug_backtrace()` (no `['args']`). `getTraceAsString()` still inlines scalar
  args truncated to 15 chars — a token *prefix* can leak. Prefer the arg-free
  form.

## 3. Keep secrets out of `custom_id`

Component `custom_id`s are sent to Discord and visible in the client. Capture the
agent/session token in the **server-side listener closure**, never in the id or a
button label. (NHA's `AgentObservation::toContainer($nha, $token)` does this
right — `$token` lives only in the `$submit` closure.)

## 4. OAuth2 / CSRF / webhooks

- **State / session ids**: `bin2hex(random_bytes(16))`, never `uniqid()`
  (predictable microtime).
- **Compare with `hash_equals()`**, not `===` / `!==` — HMAC signatures and CSRF
  state tokens. Reject empty/absent values up front; make the state single-use
  (delete it after a successful exchange).
- **Webhook signature**: verify with `hash_equals()`, prefer
  `X-Hub-Signature-256`, and **fail closed** when the shared secret env var is
  unset (an unset secret makes `hash_hmac` key `''` and every request "valid").
- **Redirect URIs**: always validate the effective `redirect_uri` against an
  allow-list — a caller-supplied value must not bypass the check (open redirect /
  auth-code interception).
- **Don't log** the OAuth authorization `code` (an exchangeable credential), the
  full request header set on a rejected request (a mistyped `Authorization` lands
  in the log), or contact-form email/message bodies (PII — send them to the
  private channel, not the log).
- Raw `curl` in an OAuth path: set `CURLOPT_SSL_VERIFYPEER => true`,
  `CURLOPT_SSL_VERIFYHOST => 2`, `CURLOPT_CONNECTTIMEOUT`, `CURLOPT_TIMEOUT`,
  `curl_close()`, and null-check the result.

## 5. Repo hygiene

- `.gitignore` must cover `.env`, `.env.*`, `composer.lock` (in these repos),
  `/var/*` (state files with tokens), and any bespoke secret file
  (`token.php`, `/json`, `botlog.txt`).
- Verify nothing sensitive is tracked: `git ls-files | grep -iE '\.env|token|secret|\.pem|\.key|credential'`.
- Never place personal data or tokens in URL query strings (they get logged by
  proxies and land in `Referer`).

## Quick grep sweep

```
rg -n "addQuery\(\s*['\"](token|key|secret|password)" src/          # token in URL
rg -n "(debug|info|warn|error).*(token|Authorization|getBody|payload|->data\b)" src/
rg -n "!==|===" src/ | rg -i "hash_hmac|signature|hash_equals"      # non-constant-time
rg -n "uniqid\(" src/                                               # weak randomness
```

