From 88d473f6e1dcaabf57bb666add7fe944f794e2b0 Mon Sep 17 00:00:00 2001 From: archipelago Date: Tue, 6 Oct 2026 15:05:17 -0400 Subject: [PATCH] Normalize only loopback authorities in embedded app runtime URLs --- docs/post-1.9.0-progress-20261006.md | 18 ++++++++++++++++++ .../__tests__/appSessionConfig.test.ts | 16 ++++++++++++++++ .../src/views/appSession/appSessionConfig.ts | 7 ++++++- 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/docs/post-1.9.0-progress-20261006.md b/docs/post-1.9.0-progress-20261006.md index c0909d75..6b9457f9 100644 --- a/docs/post-1.9.0-progress-20261006.md +++ b/docs/post-1.9.0-progress-20261006.md @@ -862,3 +862,21 @@ User reports an HTTPS-only embedded app gate while tab mode works. Added task17, with exact node/app clarification pending. No authentication bypass or live nginx modification. The private operator report is updated, regenerated and checked at390/1440px; SCP path remains unchanged. + +### HTTPS iframe investigation: bounded findings + +A real HTTPS parent on dev with an existing session successfully loaded File +Browser in an iframe (200, no gate). Certificate trust was ignored only inside +the disposable diagnostic context; this does not qualify normal browser trust. +Yaya's File Browser HTTPS port resets the connection in both curl and browser. +The exact operator URL/app is still needed to reproduce the reported gate. +Initial intercepted-parent probes were blocked by Chromium local-network access +checks; the corrected probe used the actual parent origin. Logs: +`/tmp/archy-https-frame-probe-2.log` and `...-3.log`. No gate/TLS settings changed. + +Separate regression tests reproduced embedded runtime-URL handling errors: +127.0.0.1/IPv6 loopback were not translated to the dashboard host, while a loose +localhost replacement could alter external hostnames or paths. Exact authority +matching fixes these errors. The 22 focused launch/stable-URL/loader tests pass +after two new failures were reproduced. This is not claimed as the live gate +incident's cause. Production UI build passes (`/tmp/archy-https-runtime-ui-build.log`). This change is not yet deployed. diff --git a/neode-ui/src/views/appSession/__tests__/appSessionConfig.test.ts b/neode-ui/src/views/appSession/__tests__/appSessionConfig.test.ts index 290eb458..b5798bd7 100644 --- a/neode-ui/src/views/appSession/__tests__/appSessionConfig.test.ts +++ b/neode-ui/src/views/appSession/__tests__/appSessionConfig.test.ts @@ -107,6 +107,22 @@ describe('appSessionConfig', () => { expect(resolveAppUrl('filebrowser', undefined, 'http://localhost:8083')).toBe('http://192.0.2.10:8083') }) + it('keeps loopback runtime addresses on the dashboard host for embedded apps', () => { + stubLocation({ hostname: 'node.example', protocol: 'https:' }) + for (const host of ['localhost', '127.0.0.1', '[::1]']) { + expect(resolveAppUrl('filebrowser', undefined, `http://${host}:8083/files?view=grid#recent`)) + .toBe('https://node.example:8083/files?view=grid#recent') + } + }) + + it('does not replace localhost text inside an external hostname or path', () => { + stubLocation({ hostname: 'node.example', protocol: 'http:' }) + expect(resolveAppUrl('filebrowser', undefined, 'http://localhost.example:8083/localhost')) + .toBe('http://localhost.example:8083/localhost') + expect(resolveAppUrl('filebrowser', undefined, 'http://media.example:8083/localhost')) + .toBe('http://media.example:8083/localhost') + }) + // The direct-port launch path (new-tab apps on desktop, the companion's // native WebView on phones) used to hardcode http:// — so a node reached // over HTTPS opened Vaultwarden and friends in cleartext. It must follow diff --git a/neode-ui/src/views/appSession/appSessionConfig.ts b/neode-ui/src/views/appSession/appSessionConfig.ts index 7e094271..526d4892 100644 --- a/neode-ui/src/views/appSession/appSessionConfig.ts +++ b/neode-ui/src/views/appSession/appSessionConfig.ts @@ -172,7 +172,12 @@ export function resolveAppUrl(id: string, routeQueryPath?: string, runtimeUrl?: } if (runtimeUrl && id !== 'netbird') { - let base = runtimeUrl.replace(/localhost/i, window.location.hostname) + // Only rewrite a loopback authority. A substring replacement also changes + // external hostnames and paths, while leaving 127.0.0.1 pointed at the viewer. + let base = runtimeUrl.replace( + /^(https?:\/\/)(localhost|127\.0\.0\.1|\[::1\])(?=[:/?#]|$)/i, + (_match, scheme: string) => `${scheme}${window.location.hostname}`, + ) // The backend reports runtime URLs as http:// because that is how the app // binds locally. On an HTTPS dashboard that is mixed content and the // frame is blocked outright — but ONLY upgrade when the gate fronts the