From ea8734a5b60eee731c97252b7b78d171534dfabf Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Fri, 4 Sep 2026 18:04:49 +0200 Subject: [PATCH] fix: give the guest JWKS https-required refusal its own clear error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An http:// JWKS URL without the opt-in returned the shared DisallowedScheme, whose message says "only http and https are permitted" — rejecting http while saying it is allowed. Add a distinct HttpsRequired variant ("URL must use https"), correct validate_guest_jwks_url's doc (the flag now opts in the scheme too, not just private ranges), and note in the Guests card that a JWKS URL must be https. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3 --- backend/windmill-common/src/guest_jwt.rs | 2 +- backend/windmill-common/src/ssrf.rs | 11 +++++++++-- .../(root)/(logged)/workspace_settings/+page.svelte | 6 +++--- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/backend/windmill-common/src/guest_jwt.rs b/backend/windmill-common/src/guest_jwt.rs index 17c75bcf2c..0b43d22765 100644 --- a/backend/windmill-common/src/guest_jwt.rs +++ b/backend/windmill-common/src/guest_jwt.rs @@ -889,7 +889,7 @@ y9rTR828ADcaZ63Ej1oL4GcqmGhODxCLy1YKKcy0FHzChqPMV6g=\n\ // a plaintext URL (which an on-path attacker could replace) is refused for its scheme. assert!(matches!( crate::ssrf::validate_guest_jwks_url("http://issuer.example.com/jwks.json").await, - Err(crate::ssrf::SsrfValidationError::DisallowedScheme(_)) + Err(crate::ssrf::SsrfValidationError::HttpsRequired) )); } } diff --git a/backend/windmill-common/src/ssrf.rs b/backend/windmill-common/src/ssrf.rs index 460db53f90..713b2e0289 100644 --- a/backend/windmill-common/src/ssrf.rs +++ b/backend/windmill-common/src/ssrf.rs @@ -20,6 +20,9 @@ pub enum SsrfValidationError { InvalidUrl(String), /// Scheme is not `http`/`https`. DisallowedScheme(String), + /// The URL uses `http` where `https` is required (guest JWKS). The private-host opt-in + /// also permits `http`, so, unlike the other scheme errors, this one the flag can fix. + HttpsRequired, /// No host in the URL. MissingHost, /// DNS resolution failed for the host. @@ -39,6 +42,9 @@ impl std::fmt::Display for SsrfValidationError { f, "URL scheme '{s}' is not allowed, only http and https are permitted" ), + SsrfValidationError::HttpsRequired => { + write!(f, "URL must use https") + } SsrfValidationError::MissingHost => write!(f, "URL must have a host"), SsrfValidationError::ResolutionFailed { host, source } => { write!(f, "Failed to resolve host '{host}': {source}") @@ -216,8 +222,8 @@ pub async fn validate_saml_metadata_url(url: &str) -> Result Result { let parsed = url::Url::parse(url).map_err(|e| SsrfValidationError::InvalidUrl(e.to_string()))?; @@ -232,6 +238,7 @@ pub async fn validate_guest_jwks_url(url: &str) -> Result {} + "http" => return Err(SsrfValidationError::HttpsRequired), scheme => return Err(SsrfValidationError::DisallowedScheme(scheme.to_string())), } diff --git a/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte b/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte index 079dcb0d64..1899829615 100644 --- a/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/workspace_settings/+page.svelte @@ -2265,9 +2265,9 @@ export async function main( exp (lifetime capped at 24h); it opens only the app named by app_path. Accepted algorithms: RS256/384/512, PS256/384/512, ES256/384. Symmetric algorithms (HS*) are refused. Configure one key, a PEM - public key or a JWKS URL. Point it at an issuer you control: any token that key - signs carrying these claims is accepted, so a shared multi-tenant issuer is not - a good fit. + public key or a JWKS URL (which must be https). Point it at an issuer you + control: any token that key signs carrying these claims is accepted, so a shared + multi-tenant issuer is not a good fit. {#snippet children({ item })}