validate client server origins
Client checks / checks (push) Canceled after 0s
Client dependency and security checks / audit (push) Canceled after 0s

This commit is contained in:
2026-10-25 06:57:31 +01:00
parent 313535fe70
commit aeae58d0d8
2 changed files with 28 additions and 6 deletions
+3
View File
@@ -4,3 +4,6 @@ Run formatting, linting, type checks, tests, and the AppImage build before submi
The client uses Prettier for formatting and `svelte-check` for the lint/check The client uses Prettier for formatting and `svelte-check` for the lint/check
gate. Run `npm run format:check`, `npm run lint`, `npm run check`, and gate. Run `npm run format:check`, `npm run lint`, `npm run check`, and
`npm run build` before pushing changes. `npm run build` before pushing changes.
The server address is treated as untrusted input. Client requests accept only
HTTP(S) origins without embedded credentials, query strings, or fragments.
+25 -6
View File
@@ -8,9 +8,23 @@
return typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window; return typeof window !== 'undefined' && '__TAURI_INTERNALS__' in window;
} }
function validatedServerAddress(value: string): string {
const candidate = value.trim();
let parsed: URL;
try {
parsed = new URL(candidate);
} catch {
throw new Error('The server address must be a valid HTTP or HTTPS URL.');
}
if (!['http:', 'https:'].includes(parsed.protocol) || parsed.username || parsed.password || parsed.search || parsed.hash || !parsed.hostname) {
throw new Error('The server address must use HTTP or HTTPS without credentials or URL data.');
}
return parsed.toString().replace(/\/$/, '');
}
async function saveNativeSession() { async function saveNativeSession() {
if (!nativeClientAvailable() || !username || !serverAddress || !sessionToken) return; if (!nativeClientAvailable() || !username || !serverAddress || !sessionToken) return;
const settings: ClientSettings = { server_address: serverAddress, username }; const settings: ClientSettings = { server_address: validatedServerAddress(serverAddress), username };
await invoke('save_client_settings', { settings }); await invoke('save_client_settings', { settings });
await invoke('store_server_session', { settings, session: sessionToken }); await invoke('store_server_session', { settings, session: sessionToken });
} }
@@ -24,7 +38,7 @@
if (!nativeClientAvailable()) return; if (!nativeClientAvailable()) return;
try { try {
const settings = await invoke<ClientSettings>('load_client_settings'); const settings = await invoke<ClientSettings>('load_client_settings');
serverAddress = settings.server_address; serverAddress = validatedServerAddress(settings.server_address);
username = settings.username; username = settings.username;
} catch { } catch {
authError = 'Native settings could not be read. Enter the server address and username again.'; authError = 'Native settings could not be read. Enter the server address and username again.';
@@ -50,10 +64,12 @@
async function restoreNativeSession() { async function restoreNativeSession() {
if (!nativeClientAvailable() || !username || !serverAddress) return; if (!nativeClientAvailable() || !username || !serverAddress) return;
try { try {
const settings: ClientSettings = { server_address: serverAddress, username }; const safeServerAddress = validatedServerAddress(serverAddress);
serverAddress = safeServerAddress;
const settings: ClientSettings = { server_address: safeServerAddress, username };
const storedSession = await invoke<string | null>('load_server_session', { settings }); const storedSession = await invoke<string | null>('load_server_session', { settings });
if (!storedSession) return; if (!storedSession) return;
const response = await fetch(`${serverAddress}/api/v1/auth/session`, { const response = await fetch(`${safeServerAddress}/api/v1/auth/session`, {
headers: { authorization: `Bearer ${storedSession}` }, headers: { authorization: `Bearer ${storedSession}` },
}); });
if (!response.ok) { if (!response.ok) {
@@ -325,7 +341,9 @@
authBusy = true; authBusy = true;
authError = ''; authError = '';
try { try {
const response = await fetch(`${serverAddress}/api/v1/auth/login`, { const safeServerAddress = validatedServerAddress(serverAddress);
serverAddress = safeServerAddress;
const response = await fetch(`${safeServerAddress}/api/v1/auth/login`, {
method: 'POST', method: 'POST',
headers: { 'content-type': 'application/json' }, headers: { 'content-type': 'application/json' },
body: JSON.stringify({ username, password }), body: JSON.stringify({ username, password }),
@@ -449,7 +467,8 @@
function connectRealtime() { function connectRealtime() {
realtimeSocket?.close(); realtimeSocket?.close();
const socketUrl = serverAddress.replace(/^http/, 'ws') + '/api/v1/ws'; const safeServerAddress = validatedServerAddress(serverAddress);
const socketUrl = safeServerAddress.replace(/^http/, 'ws') + '/api/v1/ws';
realtimeState = 'CONNECTING'; realtimeState = 'CONNECTING';
const socket = new WebSocket(socketUrl); const socket = new WebSocket(socketUrl);
realtimeSocket = socket; realtimeSocket = socket;