Skip to content

[v1.30.x] Configure CHASM internal callback namespace binding (#11876) - #12318

Open
davidporter-id-au wants to merge 4 commits into
temporalio:release/v1.30.xfrom
davidporter-id-au:cp-11876-v1.30
Open

davidporter-id-au wants to merge 4 commits into
temporalio:release/v1.30.xfrom
davidporter-id-au:cp-11876-v1.30

Conversation

@davidporter-id-au

@davidporter-id-au davidporter-id-au commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

What

Backport of #11876 (a210eeb) to release/v1.30.x. Same change as #12319 (the release/v1.31.x backport); this branch's workflow_handler.go was identical at the relevant call sites, so the port carried over cleanly.

Rejects temporal://internal completion 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.x port (#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.x predates the shared callback validator the same way release/v1.31.x does, and already has chasm.NexusCompletionHandlerURL, persistencespb.ChasmComponentRef, tokenspb.NexusOperationCompletion, and commonnexus.CallbackTokenHeader.

Why?

Internal callbacks must be bound to the namespace they were created in.

How did you test it?

  • Cherry-picked and extended unit test(s): table test covering same/different namespace for both token formats, missing/malformed token, missing business ID, a temporal://system passthrough, and a token crafted to read differently as a bare ref vs. an envelope
  • go build ./service/frontend/...
  • go test -tags test_dep ./service/frontend/...

Rollback

Nothing is persisted, so rolling back is safe.

🤖 Generated with Claude Code

@davidporter-id-au
davidporter-id-au requested review from a team as code owners October 1, 2026 02:22
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Claude finished @davidporter-id-au's task in 52s —— View job


Reviewed 2 files, 1 finding.

Comment thread service/frontend/workflow_handler.go Outdated
Comment on lines +5629 to +5631
if proto.Unmarshal(raw, completion) == nil && len(completion.GetComponentRef()) > 0 &&
proto.Unmarshal(completion.GetComponentRef(), ref) == nil {
return ref, nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 as NexusOperationCompletion.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/ref are empty and request_id is 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.

davidporter-id-au and others added 2 commits October 3, 2026 12:52
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>
davidporter-id-au and others added 2 commits October 3, 2026 13:08
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>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant