[v1.30.x] Configure CHASM internal callback namespace binding (#11876) - #12318
davidporter-id-au wants to merge 4 commits into
Conversation
|
Claude finished @davidporter-id-au's task in 52s —— View job Reviewed 2 files, 1 finding. |
| if proto.Unmarshal(raw, completion) == nil && len(completion.GetComponentRef()) > 0 && | ||
| proto.Unmarshal(completion.GetComponentRef(), ref) == nil { | ||
| return ref, nil |
There was a problem hiding this comment.
Namespace check can be bypassed: a single token can decode both ways, and the validator picks a different ref than history does.
The envelope parse runs first and wins if it succeeds, but a bare ChasmComponentRef usually also parses as a NexusOperationCompletion:
ChasmComponentRef.component_path(field 6,repeated string) has the same wire type asNexusOperationCompletion.component_ref(field 6,bytes).- Fields 1–3 are strings in both messages.
Failure scenario: a caller sends a bare ref {namespace_id: <victim ns>, business_id: X, component_path: [<marshaled ChasmComponentRef{namespace_id: <caller ns>, business_id: Y}>]}.
- Here, the envelope parse succeeds and returns the inner ref, which passes the same-namespace check.
- On this branch, history (
chasm/lib/callback/chasm_invocation.go:71-83) only decodes the raw bytes as a bare ref, so it uses the victim namespace. That's exactly what this check is supposed to stop.
The validator has to read the token the same way the consuming history does, or reject tokens that are ambiguous. Options:
- Check both readings when both parse: run the bare-ref reading through the namespace/business-ID checks whenever it decodes, and also check the envelope's inner ref if it's there. Reject if either one targets another namespace.
- Or, for the envelope reading, also require the envelope's own fields to look like an envelope and not a bare ref. For example,
namespace_id/workflow_id/run_id/refare empty andrequest_idis set. That isn't airtight, though, because a crafted bare ref can still match.
The first option is the simplest and closes the hole. Please add a test case built like the token above.
Backport of a210eeb to release/v1.31.x. Rejects temporal://internal completion callbacks whose CHASM component ref targets a namespace other than the request namespace, and structurally validates the ref. Hand-ported: the validator lives in a different place on this branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A legacy bare ChasmComponentRef token can decode without error as a NexusOperationCompletion envelope, since protobuf-go stores field/wire-type mismatches as unknown fields rather than erroring. Require no unknown fields before accepting the envelope decode, since a legacy ref always sets archetype_id (which collides with the envelope's message-typed ref field). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
d6a8859 to
70865e1
Compare
A bare ChasmComponentRef and a NexusOperationCompletion envelope share a wire type on field 6 (component_path vs. component_ref), so a bare ref can be crafted to also decode as an envelope wrapping a different, attacker-chosen ref. The validator was picking one reading and checking only that one, while history (chasm_invocation.go) always acts on the bare reading -- letting a forged token pass validation under one namespace while directing the actual callback at another. Validate every reading the token's bytes can produce instead of picking one, rejecting if any targets a different namespace. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
What
Backport of #11876 (a210eeb) to
release/v1.30.x. Same change as #12319 (therelease/v1.31.xbackport); this branch'sworkflow_handler.gowas identical at the relevant call sites, so the port carried over cleanly.Rejects
temporal://internalcompletion callbacks whose CHASM component ref targets a different namespace than the request namespace, and checks that the ref has a namespace ID and a business ID.This is largely fixing a callback problem for a feature that's not released, so it's expected to be low-risk.
Port notes
Cherry-picked from the
release/v1.31.xport (#12319), plus the same follow-up: the token can be read two ways (a bare ref, or an envelope wrapping a ref), and history always acts on the bare-ref reading. The validator checks every reading the bytes produce rather than picking one, so validation always agrees with what history will actually use.No manual adaptation was needed beyond that —
release/v1.30.xpredates the shared callback validator the same wayrelease/v1.31.xdoes, and already haschasm.NexusCompletionHandlerURL,persistencespb.ChasmComponentRef,tokenspb.NexusOperationCompletion, andcommonnexus.CallbackTokenHeader.Why?
Internal callbacks must be bound to the namespace they were created in.
How did you test it?
temporal://systempassthrough, and a token crafted to read differently as a bare ref vs. an envelopego build ./service/frontend/...go test -tags test_dep ./service/frontend/...Rollback
Nothing is persisted, so rolling back is safe.
🤖 Generated with Claude Code