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
|
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
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user