fix: restrict logout redirect to whitelisted domains (#8524)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-03-25 14:51:13 +00:00
committed by GitHub
parent 8a32322c18
commit 4c8edd5e94
5 changed files with 65 additions and 3 deletions
+1
View File
@@ -16386,6 +16386,7 @@ dependencies = [
"tokio",
"tower-cookies",
"tracing",
"url",
"windmill-api-auth",
"windmill-audit",
"windmill-common",
+1
View File
@@ -34,3 +34,4 @@ time.workspace = true
tokio.workspace = true
tower-cookies.workspace = true
tracing.workspace = true
url.workspace = true
+34 -2
View File
@@ -49,13 +49,13 @@ use windmill_common::users::truncate_token;
use windmill_common::users::COOKIE_NAME;
use windmill_common::utils::paginate;
use windmill_common::worker::CLOUD_HOSTED;
use windmill_common::BASE_URL;
use windmill_common::{
auth::{get_folders_for_user, get_groups_for_user},
db::UserDB,
error::{self, Error, JsonResult, Result},
utils::{not_found_if_none, rd_string, require_admin, Pagination, StripPath},
};
use windmill_common::{BASE_URL, HUB_BASE_URL};
use windmill_git_sync::handle_deployment_metadata;
const COOKIE_PATH: &str = "/";
@@ -577,12 +577,44 @@ async fn logout(
}
tx.commit().await?;
if let Some(rd) = rd {
Ok((StatusCode::TEMPORARY_REDIRECT, [(LOCATION, rd)]).into_response())
if is_valid_logout_redirect(&rd).await {
Ok((StatusCode::TEMPORARY_REDIRECT, [(LOCATION, rd)]).into_response())
} else {
tracing::warn!("Blocked logout redirect to non-whitelisted URL: {}", rd);
Ok((StatusCode::OK, "logged out successfully".to_string()).into_response())
}
} else {
Ok((StatusCode::OK, "logged out successfully".to_string()).into_response())
}
}
async fn is_valid_logout_redirect(rd: &str) -> bool {
// Allow relative paths (same-origin redirects)
if rd.starts_with('/') && !rd.starts_with("//") {
return true;
}
let parsed = match url::Url::parse(rd) {
Ok(u) => u,
Err(_) => return false,
};
let host: &str = match parsed.host_str() {
Some(h) => h,
None => return false,
};
if host == "windmill.dev" || host.ends_with(".windmill.dev") {
return true;
}
let hub_url = HUB_BASE_URL.read().await.clone();
if let Ok(hub_parsed) = url::Url::parse(&hub_url) {
if let Some(hub_host) = hub_parsed.host_str() {
if host == hub_host {
return true;
}
}
}
false
}
async fn whoami(
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
+23
View File
@@ -0,0 +1,23 @@
import { get } from 'svelte/store'
import { hubBaseUrlStore } from './stores'
export function isValidLogoutRedirect(url: string): boolean {
if (url.startsWith('/') && !url.startsWith('//')) {
return true
}
try {
const parsed = new URL(url)
const host = parsed.hostname
if (host === 'windmill.dev' || host.endsWith('.windmill.dev')) {
return true
}
const hubBaseUrl = get(hubBaseUrlStore)
try {
const hubHost = new URL(hubBaseUrl).hostname
if (host === hubHost) {
return true
}
} catch {}
} catch {}
return false
}
@@ -2,6 +2,7 @@
import { page } from '$app/state'
import CenteredModal from '$lib/components/CenteredModal.svelte'
import { clearUser } from '$lib/logout'
import { isValidLogoutRedirect } from '$lib/logoutRedirect'
import { userStore } from '$lib/stores'
import { onMount } from 'svelte'
@@ -29,7 +30,11 @@
return
}
window.location.href = rd ?? '/user/login'
if (rd && isValidLogoutRedirect(rd)) {
window.location.href = rd
} else {
window.location.href = '/user/login'
}
})
</script>