validate client server origins
This commit is contained in:
@@ -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
|
||||
gate. Run `npm run format:check`, `npm run lint`, `npm run check`, and
|
||||
`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
@@ -8,9 +8,23 @@
|
||||
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() {
|
||||
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('store_server_session', { settings, session: sessionToken });
|
||||
}
|
||||
@@ -24,7 +38,7 @@
|
||||
if (!nativeClientAvailable()) return;
|
||||
try {
|
||||
const settings = await invoke<ClientSettings>('load_client_settings');
|
||||
serverAddress = settings.server_address;
|
||||
serverAddress = validatedServerAddress(settings.server_address);
|
||||
username = settings.username;
|
||||
} catch {
|
||||
authError = 'Native settings could not be read. Enter the server address and username again.';
|
||||
@@ -50,10 +64,12 @@
|
||||
async function restoreNativeSession() {
|
||||
if (!nativeClientAvailable() || !username || !serverAddress) return;
|
||||
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 });
|
||||
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}` },
|
||||
});
|
||||
if (!response.ok) {
|
||||
@@ -325,7 +341,9 @@
|
||||
authBusy = true;
|
||||
authError = '';
|
||||
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',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ username, password }),
|
||||
@@ -449,7 +467,8 @@
|
||||
|
||||
function connectRealtime() {
|
||||
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';
|
||||
const socket = new WebSocket(socketUrl);
|
||||
realtimeSocket = socket;
|
||||
|
||||
Reference in New Issue
Block a user