fix: cap rendered webhook thread keys

This commit is contained in:
chengyongru 2026-06-25 18:26:15 +08:00
parent f67506dadf
commit 806ab0ce33
3 changed files with 47 additions and 2 deletions

View File

@ -2009,7 +2009,7 @@ Common template variables:
| `webhooks.routes.<name>.to` | empty | Required target address in `channel:chat` format. |
| `webhooks.routes.<name>.events` | `[]` | Optional provider event-name allowlist. For GitHub this matches `X-GitHub-Event`, such as `pull_request`. |
| `webhooks.routes.<name>.actions` | `[]` | Optional JSON payload `action` allowlist, such as `opened` or `synchronize`. |
| `webhooks.routes.<name>.thread` | empty | Optional Jinja template for the session key. Defaults to `to`. |
| `webhooks.routes.<name>.thread` | empty | Optional Jinja template for the session key. Defaults to `to`; rendered values are capped at 512 characters. |
| `webhooks.routes.<name>.prompt` | empty | Optional Jinja template for the inbound agent message. |
| `webhooks.routes.<name>.sender` | `webhook` | Sender ID placed on the inbound message. |
| `webhooks.routes.<name>.maxBodyBytes` | `1048576` | Maximum request body size, from 1 KiB to 10 MiB. |

View File

@ -19,6 +19,7 @@ from nanobot.utils.helpers import truncate_text
_HMAC_PREFIX = "sha256="
_DEFAULT_PROMPT_MAX_CHARS = 24_000
_DEFAULT_THREAD_MAX_CHARS = 512
_REDACTED_HEADERS = {
"authorization",
"cookie",
@ -494,7 +495,10 @@ def _render_thread(route: WebhookRouteConfig, context: dict[str, Any]) -> str:
rendered = _jinja().from_string(template).render(**context)
except TemplateError as exc:
raise WebhookError(400, f"webhook thread template failed: {exc}") from exc
return rendered.strip()
rendered = rendered.strip()
if len(rendered) > _DEFAULT_THREAD_MAX_CHARS:
raise WebhookError(400, "webhook thread template rendered too long")
return rendered
def _jinja() -> Environment:

View File

@ -218,6 +218,47 @@ async def test_webhook_custom_path_and_thread_template() -> None:
assert msg.session_key_override == "deploy:api:delivery-2"
@pytest.mark.asyncio
async def test_webhook_rejects_overlong_thread_template_without_dedupe() -> None:
bus = MessageBus()
router = WebhookRouter(
WebhooksConfig(
routes={
"deploy": WebhookRouteConfig(
auth="none",
to="websocket:ops",
thread="deploy:{{ event.thread }}",
prompt="Deploy {{ event.service }}",
)
}
),
bus,
)
failed = await router.handle(
method="POST",
path="/webhooks/deploy",
headers={"X-Nanobot-Delivery": "delivery-3"},
body=json.dumps({"service": "api", "thread": "x" * 600}).encode(),
)
accepted = await router.handle(
method="POST",
path="/webhooks/deploy",
headers={"X-Nanobot-Delivery": "delivery-3"},
body=json.dumps({"service": "api", "thread": "release"}).encode(),
)
assert failed is not None
assert failed.status == 400
assert "thread template rendered too long" in failed.body["error"]
assert accepted is not None
assert accepted.status == 202
assert accepted.body["queued"] is True
assert bus.inbound_size == 1
msg = await bus.consume_inbound()
assert msg.session_key_override == "deploy:release"
@pytest.mark.asyncio
async def test_github_webhook_validates_signature_and_dedupes_delivery() -> None:
bus = MessageBus()