Skip to content

fix(portal): preview a shared file same-origin whatever origin its url carries - #2737

Closed
L4XB wants to merge 1 commit into
Abilityai:devfrom
L4XB:fix/2733-portal-preview-same-origin
Closed

L4XB wants to merge 1 commit into
Abilityai:devfrom
L4XB:fix/2733-portal-preview-same-origin

Conversation

@L4XB

@L4XB L4XB commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Description

The portal Files rail previewed a shared file by fetching download_url, which portal_documents builds off the resolved portal base URL. On a deployment that fronts agents on a dedicated public hostname, that url carries another origin than the portal page, so CSP connect-src refused the fetch before it was sent. Download kept working, being an anchor navigation.

sameOriginPath now reduces a url under /api/ to ${pathname}${search} even when its origin differs from the page's, so the preview fetch lands on the portal page's own origin and stays inside connect-src 'self'. That origin proxies /api/* to the same backend, and a share's authority is its ?sig= token rather than the hostname the url was minted with, so the origin is decoration for these routes. Any other cross-origin url is still returned unchanged.

This is the frontend-only fix the issue names as preferred. Widening the CSP is not available: the portal base URL is a runtime setting and cannot be baked into a static nginx or Vite header.

Related Issue

Fixes #2733

Journey Impact

Journey Impact: none: the client portal's Files rail preview is not one of the eleven promises in tests/journeys/catalog.yaml. This restores an existing surface on a supported deployment topology and adds no user-facing capability.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Acceptance criteria

  • Preview works cross-origin, no connect-src violation. The fetch target is now /api/files/<id>?sig=…&preview=1, same-origin by construction.
  • Same-origin, path and query only, whatever origin download_url carries. sharePreviewPath('https://files.example.com/api/files/f1?sig=t&download=1', 'https://app.example.com') returns /api/files/f1?sig=t&download=1&preview=1. The ?sig= token and preview=1 survive, so the transfer still does not count as a download.
  • download_url unchanged. Nothing in the diff touches portal_documents or the anchor path; PortalRailFiles.vue still assigns a.href = row.item.download_url, and a test pins that the string handed to sharePreviewPath is not mutated.
  • Client uploads unaffected. They have no url and go through portal.fetchUploadBlob, which this diff does not touch.
  • No portal base URL configured keeps working. A relative download_url was already reduced to a path and still is, pinned by its own case.
  • A unit test pins the behaviour. Five cases in portalFiles.spec.js plus tests/unit/test_2733_portal_preview_same_origin.py, the sibling of test_1400_csp_blob_preview.py, which asserts that preview does not depend on the portal base URL's origin: the rail previews through the rewrite rather than off download_url, the /api/ exemption exists, Download keeps the absolute url, and neither CSP interpolates a deployment-specific origin.
  • Regression note next to the sameOriginPath comment. It said a portal base URL pointing elsewhere was "the deployment's problem"; it now records the topology as supported, with the issue number.

Testing

  • I have tested this locally
  • New tests added (if applicable)
  • All existing tests pass
npx vitest run tests/unit/portalFiles.spec.js     # 56 passed
pytest tests/unit/test_2733_portal_preview_same_origin.py tests/unit/test_1400_csp_blob_preview.py   # 8 passed

Negative control: replacing the new condition with the old if (crossOrigin) return raw fails three vitest cases and the Python guard, so the tests hold the fix rather than the surroundings.

One reversed assertion, called out rather than buried. portalFiles.spec.js pinned the old behaviour twice, in leaves a genuinely cross-origin url alone rather than pretending and at the end of marks full-blob previews without changing the download link or bearer token. The first now uses a non-/api/ url, which is where that rule still holds, and the second expects the path. Both carry a comment naming #2733, since the issue states this topology is supported rather than a misconfiguration.

What I could not verify. I have no deployment with a split portal hostname, so the browser half of AC 1 is not something I observed. The evidence here is the unit behaviour of the rewrite, the CSP guard, and the reading of service.py::portal_documents and security-headers.conf the issue lays out.

Checklist

  • My code follows the project's style guidelines
  • I have updated the documentation (if applicable) — bug fix, so a descriptive commit message per CONTRIBUTING
  • I have not committed any sensitive data (API keys, credentials, etc.)
  • I have added appropriate logging for new functionality — none added; this is a pure path rewrite

…l carries

Every `download_url` is built off the resolved portal base URL, so a
deployment that fronts its agents on a dedicated public hostname hands the
portal page a cross-origin url. The preview `fetch()` was then refused by CSP
`connect-src` before it left the browser, and the CSP cannot be the lever: the
portal base URL is a runtime setting, not a build-time constant, so no static
nginx or Vite header can allowlist it. Download was unaffected, being an
anchor navigation that `connect-src` does not govern.

`sameOriginPath` now reduces a url under `/api/` to a path even when its
origin differs from the page's. The portal page's own origin proxies those
routes to the same backend, and a share's authority is its `?sig=` token
rather than the hostname the url was minted with, so the origin is decoration
there. Any other cross-origin url is still returned unchanged: reducing one to
a path would aim the fetch at a route this origin does not serve.

The doc comment called a portal base URL pointing elsewhere "the deployment's
problem". It is a supported topology, so the comment now says that with the
issue number, and the vitest case that pinned the old behaviour is reversed
rather than deleted, with the same note.

Fixes Abilityai#2733
@vybe

vybe commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Thank you for this — you were first to #2733, and your diagnosis (the fetch leaves the page origin, connect-src 'self' refuses it, Download survives because an anchor navigation isn't governed by connect-src) was exactly right, as was the call that widening CSP isn't available when the origin is a runtime setting.

Two fixes for the same bug were open, and the merge train validated both (no defect in either — yours is correct on every input portal_documents produces today). We merged #2738, which rewrites narrowly in sharePreviewPath from /api/files/ onward and leaves sameOriginPath's contract untouched: the slice is the exact inverse of the server's f"{base}/api/files/{fid}", so a path-prefixed portal base (https://host/trinity) also resolves, and the three docs/comments that stated the now-false Range/CORS rationale are corrected in the same change. Closing this one as superseded — the fix you described is on dev now.

If you'd like a follow-up: the _-glued hex edge you might have spotted in the scanner (#2732) landed today too, and Fixes #2718 is yours.

@vybe vybe closed this Sep 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants