gateway: harden internal-route block (segment-aware check, config normalization, shared client) #385
No reviewers
Labels
No labels
Compat/Breaking
Component/Enclave-Builder
Component/Infrastructure
Deploy/BYOC
Deploy/Full-managed
Deploy/Self-hosted
Interface/API
Interface/CLI
Interface/Git
Interface/Web
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority/Critical
Priority/High
Priority/Low
Priority/Medium
Reviewed/Confirmed
Reviewed/Duplicate
Reviewed/Invalid
Reviewed/Won't Fix
Status/Abandoned
Status/Blocked
Status/Need More Info
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
public/platform!385
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "refs/pull/385/head"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Fixes a cross-tenant authorization bypass in the gateway.
Previously, any authenticated user could request
/api/internal/*. The gateway would automatically attachX-Internal-Service-Secret, causing the API to trust the request as an internal service call.Impact
An authenticated user could potentially:
Organization operations required knowing the target UUID, but did not enforce membership or ownership.
Fix
/internaland/internal/*targets in the public gateway proxy.404before reading the body, injecting credentials or contacting the API.Direct service-to-service calls to private
api:8080/internal/*remain unchanged.Tests
404, normal API traffic still works and direct service-authenticated calls remain available.Follow-up
8080remains private in every environment.Security review notes (focused on gateway/internal-route isolation):
Thanks for jumping on this — the main
/api/internal/*exposure is real, and the fix is directionally good: the proxy now blocks canonical internal targets before reading the body or attaching service credentials, uses URL path-segment normalization instead of a raw prefix check, does not forward user-supplied internal-secret headers, and adds useful unit/e2e coverage.I would still address one proxy-hardening issue before treating this as fully closed:
Medium/High: secret-bearing proxy requests still follow redirects
src/gateway/src/main.rsconstructs the shared proxy client withreqwest::Client::new(), andsrc/gateway/src/proxy.rsattachesX-Internal-Service-Secretfor authenticated proxied API requests before calling.send().reqwestfollows redirects by default, and custom headers are preserved across redirects. I verified this locally with a two-server test: a request carryingX-Internal-Service-Secretredirected cross-origin and the second server received the header.If any proxied API endpoint can return, or be induced to return, an attacker-controlled redirect, the gateway may follow it server-side with the internal service secret/request body attached. That is a secret-exposure and SSRF-hardening gap adjacent to the issue this PR fixes.
Recommended fix: use a proxy client with redirects disabled, e.g.:
Then the gateway should return backend 3xx responses to the caller instead of following them while carrying internal credentials. I’d apply this to the API proxy client at minimum; disabling redirects for gateway service proxying generally is probably safest unless there is a specific trusted redirect requirement.
Lower-severity notes
proxy_handler, after the/apiauth/sign/username-gate middleware. That means authenticated users are blocked correctly, but unauthenticated requests get401before the internal-path404, and placeholder-username users may see gate behavior before404. This does not make the route executable, but if the invariant is “public gateway never acknowledges internal paths,” consider moving the rejection into middleware that runs before auth/gating, or document/test the current behavior./%69nternal/...,/%2finternal/...,/%5Cinternal/...,/internal;param/..., and repeated/double-encoded variants. I did not confirm these as Axum bypasses; this is mainly assurance hardening.git diff --check origin/main...HEADreports trailing whitespace in changed CLI lines (src/cli/src/lib.rsaround 4236, 4244, 4254, 7099, 7314, 8086, 8133, 8198).Checks I ran locally:
Thanks — all addressed in
d1e759c.redirect::Policy::none();Locationadded to relayed response headers so callers follow 3xx themselves without the internal secret.matchitstaying non-decoding, the gateway now blocks conservatively itself — folds percent-encoding + backslashes and resolves./..with a root-clamped stack, so/%2finternal,/%69nternal,/foo%2f..%2f%69nternal, and..;matrixvariants are all rejected regardless of downstream routing. Double-encoding is the documented stopping point./api/*already returns a uniform401, so moving it pre-auth would make internal paths the one surface answering404while everything else answers401, which acknowledges them more, not less.