From f69c92e9a86d93ebbd63f2699b653b591acf93b7 Mon Sep 17 00:00:00 2001 From: ProgrammerIn-wonderland Date: Wed, 3 Jun 2026 21:11:27 -0400 Subject: [PATCH] Fix issues --- src/backend/services/auth/AuthService.test.ts | 14 ++++++++++++++ src/backend/services/auth/AuthService.ts | 10 ++++++++++ src/backend/stores/app/AppStore.js | 8 ++++++++ src/gui/src/UI/UIWindowLogin.js | 2 +- src/gui/src/UI/UIWindowSignup.js | 2 +- 5 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/backend/services/auth/AuthService.test.ts b/src/backend/services/auth/AuthService.test.ts index 4b4f677d6..63ffc7165 100644 --- a/src/backend/services/auth/AuthService.test.ts +++ b/src/backend/services/auth/AuthService.test.ts @@ -1299,6 +1299,20 @@ describe('AuthService (integration)', () => { expect(a).toBe(b); expect(a).toMatch(/^app-/); }); + + it.each([ + 'javascript:alert(document.domain)', + 'data:text/html,', + 'file:///etc/passwd', + 'vbscript:msgbox(1)', + ])('throws 400 for non-http(s) scheme %s', async (origin) => { + // These parse fine via `new URL()` but must never become a + // bootstrap app `index_url` — that would be a stored XSS / + // code-execution vector when launched as `iframe.src`. + await expect( + authService.appUidFromOrigin(origin), + ).rejects.toMatchObject({ statusCode: 400 }); + }); }); describe('getUserAppToken', () => { diff --git a/src/backend/services/auth/AuthService.ts b/src/backend/services/auth/AuthService.ts index 885b55baa..e879313c3 100644 --- a/src/backend/services/auth/AuthService.ts +++ b/src/backend/services/auth/AuthService.ts @@ -1633,6 +1633,16 @@ export class AuthService extends PuterService { #originFromUrl(url: string): string | null { try { const parsed = new URL(url); + // A real web origin is always http(s). `new URL()` happily parses + // `javascript:`, `data:`, `file:`, `vbscript:`, etc.; if one of + // those slips through it ends up persisted as an app `index_url` + // (see AppStore.createFromOrigin) and later loaded as `iframe.src` + // — an XSS/code-execution primitive. Reject anything that isn't + // http(s) so the bootstrap path matches AppDriver's validateUrl + // allow-list. + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') { + return null; + } return `${parsed.protocol}//${parsed.hostname}${parsed.port ? `:${parsed.port}` : ''}`; } catch { return null; diff --git a/src/backend/stores/app/AppStore.js b/src/backend/stores/app/AppStore.js index c114b5447..576c29309 100644 --- a/src/backend/stores/app/AppStore.js +++ b/src/backend/stores/app/AppStore.js @@ -20,6 +20,7 @@ import { v4 as uuidv4 } from 'uuid'; import { PuterStore } from '../types'; import { HttpError } from '../../core/http/HttpError.js'; +import { validateUrl } from '../../util/validation.js'; /** * Persistence + cache for the `apps` table. @@ -375,6 +376,13 @@ export class AppStore extends PuterStore { * `#isOriginBootstrapApp` checks. */ async createFromOrigin(uid, origin) { + // Bootstrap apps persist `origin` straight into `index_url`, which is + // later loaded as `iframe.src`. Enforce the same http(s) scheme + // allow-list as the AppDriver create/update path so a caller-supplied + // `javascript:`/`data:`/`file:` origin can never become a stored, + // launchable code-execution vector. + validateUrl(origin, { key: 'origin' }); + const fields = { name: uid, title: uid, diff --git a/src/gui/src/UI/UIWindowLogin.js b/src/gui/src/UI/UIWindowLogin.js index 0ff53f3e4..19a2b444d 100644 --- a/src/gui/src/UI/UIWindowLogin.js +++ b/src/gui/src/UI/UIWindowLogin.js @@ -356,7 +356,7 @@ async function UIWindowLogin (options) { }, onAppend: function (this_window) { if ( options.authError ) { - $(this_window).find('.login-error-msg').html(options.authError).fadeIn(); + $(this_window).find('.login-error-msg').html(html_encode(options.authError)).fadeIn(); } if ( ! window.disable_login_autofocus ) { diff --git a/src/gui/src/UI/UIWindowSignup.js b/src/gui/src/UI/UIWindowSignup.js index 8219103d7..6b390f832 100644 --- a/src/gui/src/UI/UIWindowSignup.js +++ b/src/gui/src/UI/UIWindowSignup.js @@ -138,7 +138,7 @@ function UIWindowSignup (options) { center: true, onAppend: function (el_window) { if ( options.authError ) { - $(el_window).find('.signup-error-msg').html(options.authError).fadeIn(); + $(el_window).find('.signup-error-msg').html(html_encode(options.authError)).fadeIn(); } if ( ! window.disable_signup_autofocus ) {