{"record":{"id":"bb29afd79ab4c350","repo":"hcengineering/platform","slug":"invalid-hook-token","errorCode":null,"errorMessage":"Invalid hook token","messagePattern":"Invalid hook token","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"services/mail/pod-mail-worker/src/handlerMta.ts","lineNumber":41,"sourceCode":"  isHulyMessage,\n  generateNewEmailId,\n  MailHeader\n} from '@hcengineering/mail-common'\nimport { getClient as getAccountClient } from '@hcengineering/account-client'\nimport { createRestTxOperations } from '@hcengineering/api-client'\n\nimport { mailServiceToken, baseConfig, kvsClient } from './client'\nimport config from './config'\nimport { MtaMessage, HulyMessageType } from './types'\nimport { getHeader, parseContent } from './utils'\nimport { decodeEncodedWords } from './decode'\n\nexport async function handleMtaHook (req: Request, res: Response, ctx: MeasureContext): Promise<void> {\n  try {\n    if (config.hookToken !== undefined) {\n      const token = req.headers['x-hook-token']\n      if (token !== config.hookToken) {\n        throw new Error('Invalid hook token')\n      }\n    }\n\n    const mta: MtaMessage = req.body\n\n    const headers: string[] = mta.message.headers.map((header) => header[0].trim()) ?? []\n    if (isHulyMessage(headers)) {\n      return\n    }\n\n    const from: EmailContact = getEmailContact(mta.envelope.from.address)\n    if (config.ignoredAddresses.includes(from.email)) {\n      return\n    }\n    const fromHeader = getHeader(mta, MailHeader.From)\n    if (fromHeader !== undefined) {\n      const { firstName, lastName } = extractContactName(ctx, fromHeader, from.email)\n      from.firstName = firstName","sourceCodeStart":23,"sourceCodeEnd":59,"githubUrl":"https://github.com/hcengineering/platform/blob/63e28dc96483967b2fc21c881b3f1023c1de7718/services/mail/pod-mail-worker/src/handlerMta.ts#L23-L59","documentation":"The MTA webhook handler in pod-mail-worker validates an incoming webhook by comparing the 'x-hook-token' request header against the configured hookToken (config.hookToken). When the token is configured and the header is missing, malformed, or does not exactly match, the handler rejects the request with 'Invalid hook token' to prevent unauthorized email hook injection. It is a security gate, not an internal fault.","triggerScenarios":"A POST to the MTA hook endpoint with: (1) no 'x-hook-token' header, (2) a token that differs from config.hookToken (typo, stale secret after rotation), or (3) the worker redeployed with a new HOOK_TOKEN env value while the MTA still sends the old one.","commonSituations":"Rotating the hook token in one deployment (worker or MTA) but not the other; token passed via query param instead of header; multiple MTA instances where only some were updated; local dev MTA not configured to send the header at all while the worker sets config.hookToken.","solutions":["Set the 'x-hook-token' header on the MTA webhook request to exactly match config.hookToken (HOOK_TOKEN env var)","If tokens were rotated, redeploy/reconfigure the MTA side with the new token","If you intend no auth (dev only), unset config.hookToken so the check is skipped","Log/inspect the received header value (carefully, no secrets in logs) to confirm exact match, including whitespace"],"exampleFix":"// before\ncurl -X POST https://mta-hook/url -d '{...}'\n// after\ncurl -X POST https://mta-hook/url -H \"x-hook-token: $HOOK_TOKEN\" -d '{...}'","handlingStrategy":"validation","validationCode":"function validateHookRequest(req: Request, expectedToken?: string): void {\n  if (expectedToken !== undefined && req.headers['x-hook-token'] !== expectedToken) {\n    throw new Error('Invalid hook token')\n  }\n}","typeGuard":"function hasValidHookToken(req: Request, config: { hookToken?: string }): boolean {\n  return config.hookToken === undefined || req.headers['x-hook-token'] === config.hookToken\n}","tryCatchPattern":"try {\n  await handleMtaHook(req, res, ctx)\n} catch (e) {\n  if (e.message === 'Invalid hook token') {\n    res.status(401).send('unauthorized')\n  } else throw e\n}","preventionTips":["Keep HOOK_TOKEN in one shared secret store referenced by both MTA and worker so rotation is atomic","Configure the MTA client template to always send the x-hook-token header","Use constant-time comparison for token checks in security-sensitive deployments","Smoke-test the webhook after every deployment"],"tags":["auth","webhook","security","configuration"],"backgroundTag":"webhook-token-mismatch","analyzedSha":"63e28dc96483967b2fc21c881b3f1023c1de7718","analyzedAt":"2026-08-29T15:21:27.377Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}