Summary
The external-app runtime's unauthenticated HTML-navigation fallback can build its login redirect from the Next.js pod's internal listen URL (https://0.0.0.0:3000) rather than the public CAIPE origin.
This does not affect the normal modern-browser path when Sec-Fetch-Dest: document reaches CAIPE: that path currently returns the safe relative location /login. It also is not a confirmed defect in the explicit Sign Out button. The affected branch is the intentional fallback that treats Accept: text/html as a document navigation when Sec-Fetch-Dest is absent. This can affect clients, embedded contexts, tests, or proxy paths that omit or strip Fetch Metadata headers.
The behavior is generic to config-driven external-app routes.
Reproduction
Against a CAIPE deployment with a configured external app:
curl -sS --max-redirs 0 -D - -o /dev/null \
-H 'Accept: text/html' \
'https://<public-caipe-host>/apps/<configured-app-id>'
Observed:
HTTP/2 307
location: https://0.0.0.0:3000/login?callbackUrl=%2Fapps%2F%3Cconfigured-app-id%3E
Control case representing a modern top-level browser navigation:
curl -sS --max-redirs 0 -D - -o /dev/null \
-H 'Accept: text/html' \
-H 'Sec-Fetch-Dest: document' \
'https://<public-caipe-host>/apps/<configured-app-id>'
Observed control result:
HTTP/2 307
location: /login
Expected behavior
Every branch classified by CAIPE as a document navigation should produce the same safe, public-origin login redirect. It must never expose 0.0.0.0, localhost, a pod IP, or a cluster-local hostname.
A relative response is preferable when possible:
/login?callbackUrl=%2Fapps%2F%3Cconfigured-app-id%3E
Otherwise it should use the configured public CAIPE origin.
Relevant implementation
isDocumentNavigation() in ui/src/app/api/agentic-apps/runtime/[appId]/[[...path]]/route.ts deliberately falls back to Accept: text/html when Sec-Fetch-Dest is absent. redirectToLogin() in that same file then constructs an absolute URL from new URL(request.url), which can contain the internal bind origin behind ingress.
CAIPE already contains getRequestOrigin() in ui/src/app/api/skills/_lib/request-origin.ts. Its documentation describes this exact ingress problem and resolves the public origin from NEXTAUTH_URL, then sanitized forwarded headers, before falling back to request.url. The fix should use a shared, appropriately located public-origin implementation or return a safe relative redirect rather than adding application-specific configuration.
The generic protected layout and explicit logout/sign-out paths should be covered by regression tests as controls, even though normal browser navigation currently returns a safe relative /login.
Acceptance criteria
- The
Accept: text/html fallback without Sec-Fetch-Dest never returns an internal origin.
- Modern browser navigation with
Sec-Fetch-Dest: document continues to redirect safely.
- The original relative path and query string are retained safely in
callbackUrl wherever the runtime route owns the redirect.
- Regression tests simulate
request.url as http://0.0.0.0:3000/... and cover both document-detection branches.
- Existing callback validation and open-redirect protections remain intact.
- Explicit logout and generic protected-layout redirects remain same-origin.
- The solution is generic; no application-specific route or deployment setting is introduced.
Summary
The external-app runtime's unauthenticated HTML-navigation fallback can build its login redirect from the Next.js pod's internal listen URL (
https://0.0.0.0:3000) rather than the public CAIPE origin.This does not affect the normal modern-browser path when
Sec-Fetch-Dest: documentreaches CAIPE: that path currently returns the safe relative location/login. It also is not a confirmed defect in the explicit Sign Out button. The affected branch is the intentional fallback that treatsAccept: text/htmlas a document navigation whenSec-Fetch-Destis absent. This can affect clients, embedded contexts, tests, or proxy paths that omit or strip Fetch Metadata headers.The behavior is generic to config-driven external-app routes.
Reproduction
Against a CAIPE deployment with a configured external app:
Observed:
Control case representing a modern top-level browser navigation:
Observed control result:
Expected behavior
Every branch classified by CAIPE as a document navigation should produce the same safe, public-origin login redirect. It must never expose
0.0.0.0, localhost, a pod IP, or a cluster-local hostname.A relative response is preferable when possible:
Otherwise it should use the configured public CAIPE origin.
Relevant implementation
isDocumentNavigation()inui/src/app/api/agentic-apps/runtime/[appId]/[[...path]]/route.tsdeliberately falls back toAccept: text/htmlwhenSec-Fetch-Destis absent.redirectToLogin()in that same file then constructs an absolute URL fromnew URL(request.url), which can contain the internal bind origin behind ingress.CAIPE already contains
getRequestOrigin()inui/src/app/api/skills/_lib/request-origin.ts. Its documentation describes this exact ingress problem and resolves the public origin fromNEXTAUTH_URL, then sanitized forwarded headers, before falling back torequest.url. The fix should use a shared, appropriately located public-origin implementation or return a safe relative redirect rather than adding application-specific configuration.The generic protected layout and explicit logout/sign-out paths should be covered by regression tests as controls, even though normal browser navigation currently returns a safe relative
/login.Acceptance criteria
Accept: text/htmlfallback withoutSec-Fetch-Destnever returns an internal origin.Sec-Fetch-Dest: documentcontinues to redirect safely.callbackUrlwherever the runtime route owns the redirect.request.urlashttp://0.0.0.0:3000/...and cover both document-detection branches.