Skip to content

fix(agentic-apps): HTML-navigation fallback can redirect to an internal origin #2726

Description

@artdroz

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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/rbac-authArea: RBAC / Auth / SecuritybugSomething isn't workingui

    Type

    No type

    Projects

    • Status
      Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions