Repository navigation
Dapr Runtime v1.17.16
Pre-releaseDapr 1.17.16
This update contains the following security fix:
- Security: Service invocation delivered spoofed caller identity headers to the target app
- Security: Scheduler accepted actor reminders targeting another app's internal workflow actors
- Security: Service invocation access control bypass with percent-encoded path characters
Security: Service invocation delivered spoofed caller identity headers to the target app
Problem
On service invocation, Dapr tells the target app who called it through the dapr-caller-app-id, dapr-caller-namespace and dapr-callee-app-id headers (gRPC metadata for gRPC apps).
A calling app could send its own values for these headers, and they reached the target app alongside the values Dapr set:
Dapr-Caller-App-Id: admin
Dapr-Caller-App-Id: checkout
- Service invocation through the HTTP API: a header such as
Dapr-Caller-App-Id: admin, in any casing, reached the target app as a second value of the header, in random order. - gRPC proxying: the calling app's
dapr-caller-app-idvalue always came before the real one, and Dapr never setdapr-caller-namespace, so the target app received only the namespace the calling app sent.
Impact
You were affected if a target app makes authorisation or tenancy decisions from these headers and uses the first value it finds (as Go's http.Header.Get and metadata.Get(...)[0] in gRPC do).
Any app able to invoke the target app could then make it believe the call came from a different app or namespace.
Dapr's own access control policies, workflow access policies and metrics were not affected: they use the calling sidecar's mTLS identity, not these headers.
Root Cause
The HTTP API copies every request header into the invocation metadata and keeps the casing Go gives it, so dapr-caller-app-id sent by an app is stored as Dapr-Caller-App-Id.
The calling sidecar then set the identity headers under their lowercase names without removing other casings, so both keys travelled to the target sidecar, which merged them into one header with two values when delivering the request to the app.
Self-invocation already removed these headers in any casing, but cross-app invocation did not.
On the gRPC proxy path, the calling sidecar forwarded the app's metadata and appended its own identity values instead of replacing them, and never set dapr-caller-namespace.
The target sidecar passed all of these values to the app without checking them.
Solution
- The calling sidecar now removes these headers in any casing before setting them, on every service invocation path.
- On the gRPC proxy path, the calling sidecar replaces the identity metadata instead of appending to it, and now also sets
dapr-caller-namespace. - The target sidecar now sets these headers itself before delivering the request, so apps are protected even while calling sidecars still run an older version. This covers service invocation and actor invocation received from another sidecar. With mTLS enabled,
dapr-caller-app-idanddapr-caller-namespacecome from the calling sidecar's mTLS certificate (SPIFFE ID) and, for service invocation,dapr-callee-app-idis the target's own app ID. With mTLS disabled, the target sidecar keeps the single value set by the calling sidecar and drops the rest. - Caller identity headers in actor and workflow metadata received from the user-facing APIs are now removed in any casing.
- gRPC proxied self-invocation (an app invoking its own app ID) now carries the identity headers set by the sidecar, with caller and callee both the app itself, as HTTP self-invocation already did.
This is a behaviour change: with mTLS enabled, the caller identity headers an app receives are now always set by its own sidecar from the caller's mTLS identity, and gRPC proxied calls now carry dapr-caller-namespace.
With mTLS disabled there is no verified identity to fall back on: if the calling sidecar has not been upgraded, dapr-caller-namespace on gRPC proxied calls still reflects the value the calling app sent, so upgrade every sidecar to close the gap fully.
Security: Scheduler accepted actor reminders targeting another app's internal workflow actors
Problem
The Scheduler authorized actor reminder job requests only by checking that the job metadata namespace and app ID matched the caller's mTLS (SPIFFE) identity.
The actor type the reminder targets was not checked against the caller.
An application with a valid Dapr identity in a namespace could therefore schedule, read, list or delete reminder jobs on the reserved internal actor types of any other application in that namespace (dapr.internal.<namespace>.<appid>.workflow, .activity and so on).
Impact
A compromised or malicious application in the same namespace could:
- Trigger reminders on another application's workflow and activity actors.
The Scheduler delivers the reminder to the sidecar hosting that actor type, which executes it locally. - Run an arbitrary activity registered by the target application with attacker supplied input, by scheduling the activity execution reminder on the target application's
.activityactor type. - Inject a
TimerFiredevent, or an activity result (TaskCompletedorTaskFailedwith attacker supplied payload) into a workflow instance of the target application.
An injected result for a task the workflow had not scheduled yet was held in the workflow inbox and consumed once the workflow scheduled that task. - Forge the result of an activity the target workflow is waiting on, by scheduling an
activity-result-*reminder on the target application's.workflowactor type carrying aTaskCompletedorTaskFailedevent for the outstanding task.
The workflow actor accepted the result as if it came from the application the activity was dispatched to, so the workflow continued with attacker supplied output, or failed, while the real activity result was later discarded as a duplicate. - Delete another application's workflow reminders, stalling its workflows.
- Delete every reminder job on every internal actor type in the namespace at once, by requesting a bulk delete by metadata with an ID prefix match on the user actor type
dapr, whose key is a prefix of everydapr.internal.<namespace>.<appid>.*key. - Schedule an
activity-result-*reminder on another application's workflow actor carrying an event that is not an activity result.
A raised event, a termination or a timer firing was admitted to the workflow inbox without any creator check.
The attacker needs a valid Dapr identity in the same namespace as the target application, so the issue does not cross namespace boundaries and does not affect deployments without mTLS beyond what an unauthenticated Scheduler already allows.
Root Cause
The Scheduler's job authorization ignored the actor type of actor targeted jobs.
Only the placement service checked that an application does not claim another application's dapr.internal.<namespace>.<appid>.* actor types, and that check applies to hosting actor types, not to scheduling reminders on them.
Solution
The Scheduler now rejects with PermissionDenied any actor targeted job request (schedule, get, delete, list, delete by metadata and delete by name prefix) whose actor type is a reserved internal type dapr.internal.<namespace>.<appid>.* that does not belong to the requesting application and namespace.
User actor types are not restricted, so applications can still create reminders on actors hosted by other applications.
Activity result reminders (activity-result-*) on another application's workflow actor type in the same namespace remain allowed, since an activity host uses them to return a result to the workflow's application when the workflow's sidecar is unreachable.
That exception applies only to scheduling the job without overwrite: reading, deleting or replacing such a job on another application's workflow actor is denied, and the activity host creates these reminders without overwrite, treating an already existing job of the same name as a successful create.
Delivery of such a result is still subject to the workflow actor's creator checks described below.
The Scheduler also always terminates the actor type with the key delimiter when it builds a key prefix from job metadata, so an ID prefix match can only match actor IDs of that exact actor type and a bulk delete can no longer cross from one actor type into another.
The workflow actor now also verifies who created an activity result reminder.
The Scheduler verifies the creating application's identity when the reminder job is scheduled and sends it back to the sidecar when the reminder fires, and the sidecar passes it to the workflow actor.
An activity result reminder whose creator is neither the application the activity was dispatched to nor the workflow's own application is dropped and acknowledged, so the one-shot reminder is deleted rather than retried, and the workflow keeps waiting for the real result.
Reminders without a known creator (reminders stored in the actor state store, or delivered by a Scheduler from an older release that does not send the job metadata back) are accepted as before.
The workflow actor now drops, and acknowledges so that the reminder is deleted, any activity-result-* reminder whose payload is not a TaskCompleted or TaskFailed event, whoever created it, so no other event type can enter the inbox through that name.
An activity result created by another application for a task the workflow has not scheduled yet is no longer parked in the inbox, where it would have been consumed without a creator check once the task was scheduled.
The workflow actor drops and acknowledges it, so the reminder is deleted.
The creating application is now carried when a sidecar that received a fired reminder forwards it to the replica of the same application that hosts the actor.
The receiving sidecar trusts the forwarded creator only from a replica of its own application, as identified by the peer's mTLS identity, and otherwise records the peer application itself as the creator, so a forged reminder cannot shed its creator by being forwarded.
Security: Service invocation access control bypass with percent-encoded path characters
Problem
A service invocation method containing a percent-encoded /, \ or . (%2F, %5C or %2E) could reach an operation on the target application that the access control policy was configured to deny.
The access control check treated these sequences as literal characters, but the target application's HTTP server decoded them into path separators and dot segments, so the application served a different path from the one that was checked.
This completes the fix for CVE-2026-41491 ("Security: Service invocation path traversal bypasses access control policies"), which left these encoded characters unhandled.
Impact
You were affected if you used service invocation access control policies and the target application uses the HTTP app protocol.
An attacker needs an application identity that the policy allows to call the target, for example a lower privileged application that is denied only some operations.
- With a deny-list policy (
defaultAction: allowand operations set toaction: deny, as in the access control documentation), the attacker could invoke a denied operation such as/admin/secretby sending the methodadmin%2Fsecretthrough the gRPC API, oradmin%252Fsecretthrough the HTTP API.
The access control check saw a single segment,admin%2Fsecret, that matched no rule, so the defaultallowapplied.
The application received/admin%2Fsecretand its HTTP server decoded it to/admin/secret.
%2E%2E/adminreached/adminin the same way on applications that resolve... - With an allow-list policy that uses wildcards (
defaultAction: denyand/public/*allowed), the methodpublic/x%2F..%2F..%2Fadminmatched the wildcard and was delivered as/public/x/../../admin, which reaches/adminon applications that resolve...
Whether the denied handler ran depends on the application's HTTP framework.
Frameworks that route on the decoded path, such as Python WSGI and ASGI applications or Go handlers that read r.URL.Path, were bypassed.
Frameworks that route on the escaped path, such as the Go 1.22+ ServeMux and chi, were not.
Root Cause
The CVE-2026-41491 fix normalizes the method once, in NormalizeMethod, and uses the result for both the access control check and dispatch to the application.
It treated percent-encoded sequences as literal characters, and the access control check splits the method on / only, so admin%2Fsecret was one segment.
The HTTP app channel builds the outbound URL by appending the method to the application address, so %2F was sent to the application unchanged and decoded there.
The access control check and the application therefore still saw different paths.
gRPC callers send the method as is.
The HTTP API decodes the request URL once, so a double-encoded %252F reached the access control check as %2F.
Solution
NormalizeMethod now rejects any method that contains %2F, %5C or %2E, in any letter case, or a literal \, in the same way it already rejects #, ?, null bytes and control characters.
A literal \ is rejected because the HTTP app channel re-encodes it to %5C on the wire, so the application would decode it back into a character the access control check did not split on.
The request fails before the access control check and never reaches the application.
This covers service invocation through the HTTP and gRPC APIs, requests received from other sidecars, proxied gRPC calls, and actor method invocation.
Actor types and actor IDs on actor invocation, and actor IDs and names on reminder and timer registration, are rejected for the same encoded characters, since the actor ID is also placed in the URL path sent to the application.
Other percent-encoded sequences, such as %20 or %25, are still accepted and passed through unchanged.
Breaking change: a method that contains %2F, %5C or %2E after HTTP decoding, or a literal \, is now rejected.
This means a method double-encoded by an HTTP caller (for example /v1.0/invoke/myapp/method/a%252Fb), sent raw by a gRPC caller (for example a%2Fb or a\b), or a single-encoded backslash sent by an HTTP caller (a%5Cb, decoded to a\b).
Service invocation returns HTTP 500 or gRPC Internal with an invalid method message, and actor invocation returns HTTP 400 or gRPC InvalidArgument.
A single-encoded %2F or %2E sent to the service invocation HTTP API (/v1.0/invoke/{app}/method/{method}) is decoded to / or . before the check and still works as before.
The actor HTTP API (/v1.0/actors/{type}/{id}/method/{method}) decodes its route parameters once in the same way, so a single-encoded %2F or %2E in an actor method still decodes to / or . and is handled as before.
A single-encoded %5C decodes to a literal backslash and is now rejected with HTTP 400, as is any double-encoded %252F, %255C or %252E.
Applications that need a slash, backslash or dot in a method name beyond what the plain path allows, for example to call an external HTTP endpoint, must change how they build the method.