Conversation
bnusunny
force-pushed
the
fix/tenant-id-header-hardening
branch
from
September 25, 2026 21:27
0c74608 to
580bde7
Compare
…t value The adapter asserts `x-amz-tenant-id` from the Lambda invocation context, but it only ever set the header, never cleared one the caller had sent. When the context carried no tenant ID the block was skipped entirely, so a caller's own `x-amz-tenant-id` was forwarded to the application untouched — the opposite of what the multi-tenancy guide promised. Remove the header before the conditional insert, the same way the sibling `x-amzn-request-context` and `x-amzn-lambda-context` headers are set unconditionally. The adapter now either sets this header itself or leaves the application with none. Also rewrite the multi-tenancy guide, which is the larger half of this change. It claimed no additional configuration was required, which is wrong in a way that matters: the tenant ID only arrives when the function uses Lambda tenant isolation, which is immutable at creation time, excludes function URLs, and among HTTP triggers works only with API Gateway REST. The guide now also says where the mapped tenant ID must come from. Mapping a raw client header to `integration.request.header.X-Amz-Tenant-Id` lets any caller name their own tenant — API Gateway forwards it, Lambda puts it in the context, and the adapter faithfully asserts it — so the guide directs readers to map from an authorizer context value or verified token claim instead, and says plainly that a request header is not such a source. The guide also warns that the stripping stops where the adapter does: the same image run on Amazon ECS, Amazon EKS or a local Docker host has nothing removing a caller-supplied `X-Amz-Tenant-Id`, so an application treating it as an asserted identity there is reading raw caller input. This mirrors the caveat `README.md` already carries for the SnapStart hook routes.
bnusunny
force-pushed
the
fix/tenant-id-header-hardening
branch
from
September 25, 2026 21:37
580bde7 to
5bc3f65
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The adapter asserts
x-amz-tenant-idon the request it forwards to your application: the value is supposed to come from the Lambda invocation context, not from the caller. But the code only ever set that header. It never removed one the caller had already sent.When the invocation context carries no tenant ID, the whole block is skipped, so a caller's own
x-amz-tenant-idreaches the application untouched. The multi-tenancy guide says the opposite — "If no tenant ID is present, the header is omitted."The existing test for that promise could not catch it.
test_tenant_id_header_absent_when_no_tenantasserted the app sees no tenant header, but never set one on the inbound request, so it passed no matter what the adapter did.Why it matters
An application following the guide reads this header as the tenant identity. It should never be able to read a value the caller chose. The adapter's guarantee was narrower than the guide's wording, and the gap was invisible to the test suite.
What changed
The adapter now drops any caller-supplied
x-amz-tenant-idbefore deciding whether to set its own. After this change it either sets the header itself or leaves the application with none — there is no third case.src/lib.rsreq_headers.remove(...)before the conditional insert, matching howx-amzn-request-contextandx-amzn-lambda-contextare set unconditionallysrc/lib.rs(tests)docs/guide/src/features/multi-tenancy.mdThe guide is the larger half of this change, and it had two problems.
It ended with "No additional configuration is required," which is wrong in a way that matters. The tenant ID only arrives when the function uses Lambda tenant isolation, and that is immutable at function creation, unsupported on function URLs, and among HTTP triggers works only with API Gateway REST — HTTP APIs cannot override the header Lambda's
InvokeAPI requires. Someone who followed the old guide on a function URL would see an empty tenant on every request.It also said nothing about where the mapped tenant ID should come from. Mapping a raw client header to
integration.request.header.X-Amz-Tenant-Idlets any caller name their own tenant: API Gateway forwards the value it was handed, Lambda puts it in the invocation context, and the adapter faithfully asserts it — so the application receives a caller-chosen tenant that looks platform-asserted, and this PR's header stripping buys nothing. The guide now tells readers to map from an authorizer context value or verified token claim and says plainly that a request header is not such a source.Finally, the stripping this PR adds lives in the adapter, so it stops where the adapter does. The same image on Amazon ECS, Amazon EKS or a local Docker host has nothing removing a caller-supplied
X-Amz-Tenant-Id, which matters here because running one image everywhere is the project's headline claim. The guide carries that warning in the same formREADME.mdalready uses for the SnapStart hook routes.Backwards compatibility
Compatible:for every deployment that uses this header as documented — the adapter's own value is unchanged, and Lambda rejects an invocation of a tenant-isolated function that carries no tenant ID, so the value is always present where it is meant to be read.One behaviour does change: an application that was reading a caller-supplied
x-amz-tenant-idpassed through the adapter no longer receives it. That is the point of the change, and such a deployment was reading caller input as a tenant identity.Tests
cargo test --lib: 67 passed.cargo fmt --checkandcargo clippy --lib --all-targetsclean.test_tenant_id_header_absent_when_no_tenant— strengthened to setx-amz-tenant-id: client-suppliedon the inbound event. This is the regression guard: it fails on the pre-fix code (the app receivesclient-supplied, the mock does not match, 404) and passes after.test_context_tenant_id_wins_over_client_supplied_header— new, and deliberately not a second guard for this fix. On theSomepathinsertalready drops every prior value, so it passes with or without theremoveline. What it pins is precedence: the caller sends one value, the context carries another, and the context's value must be the only one forwarded. It asserts the complete header value set rather than "some entry matches", which keeps it meaningful ifinsertever becomesappend. Its comment says all of this, so the next reader does not mistake it for the regression test.Both assertions were mutation-checked rather than just run: deleting
req_headers.remove(...)fails the first, and deleting it while switchinginserttoappendfails both. The previous.header()matcher survived that second mutation.Manual verification
Verified against the live service in
us-west-2that the header's source behaves as the rewritten guide describes: a function created withTenantIsolationMode=PER_TENANTrejects an invocation with no tenant ID (InvalidParameterValueException) and reportscontext.tenant_idwhen given one, while a function without tenancy config reportsNoneand rejects an invocation that supplies a tenant ID. Also confirmed through an API Gateway REST API withintegration.request.header.X-Amz-Tenant-Idmapped from a client header: omitting the client header returns 400 before the function runs, and a caller-suppliedx-amz-tenant-iddoes reach the event headers, which is what this change drops.no linked issue: found while reviewing the tenant header path; the fix is small enough to stand alone.