Surfaced by the security re-review of #1983 (review 5289004003), re-verified at 85e5de84.
What
The teams intake scheme has no replay protection and no dedupe.
Microsoft's Bot Framework signature carries no timestamp, so verifyUntimestamped() is the correct verifier — there is nothing to bind the signature to a moment. That is not the problem. The problem is what compensates for it: nothing.
InboundMessage::$externalId is documented as "the channel's own id for it, unique per channel" and is used only for rule matching. It is never used as an idempotency key, and there is no store of seen ids.
Why it matters
A captured request stays valid forever. Replaying it reopens or re-routes a case indefinitely, bounded only by #[AnonRateLimit(300, 60)] — which bounds the rate, not the total, and is per-source-IP.
For an intake channel that opens cases in a government workflow, "the same signed request can be replayed for ever" is a durability problem as much as a security one: it produces duplicate cases that look legitimate, because they are legitimately signed.
Suggested fix
This is the standard compensating control for a signature scheme with no timestamp, and it is worth doing for every such scheme, not just teams.
Surfaced by the security re-review of #1983 (review 5289004003), re-verified at
85e5de84.What
The
teamsintake scheme has no replay protection and no dedupe.Microsoft's Bot Framework signature carries no timestamp, so
verifyUntimestamped()is the correct verifier — there is nothing to bind the signature to a moment. That is not the problem. The problem is what compensates for it: nothing.InboundMessage::$externalIdis documented as "the channel's own id for it, unique per channel" and is used only for rule matching. It is never used as an idempotency key, and there is no store of seen ids.Why it matters
A captured request stays valid forever. Replaying it reopens or re-routes a case indefinitely, bounded only by
#[AnonRateLimit(300, 60)]— which bounds the rate, not the total, and is per-source-IP.For an intake channel that opens cases in a government workflow, "the same signed request can be replayed for ever" is a durability problem as much as a security one: it produces duplicate cases that look legitimate, because they are legitimately signed.
Suggested fix
externalIdas an idempotency key: record it on first acceptance, and answer a repeat with the original outcome rather than routing it again.externalIdis only documented unique per channel.This is the standard compensating control for a signature scheme with no timestamp, and it is worth doing for every such scheme, not just
teams.