From 1751fb6920a98adaa593dcfae91e66d48789a89a Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Fri, 4 Sep 2026 16:15:25 +0200 Subject: [PATCH] fix: measure the guest PEM cap before trimming The cap read pem.trim(), but edit_guest_jwt_key stores the untrimmed string, so a key with 8 KiB of leading or trailing whitespace passed the check and was stored (and reparsed per request) at full size. Measure the untrimmed input. Test covers a whitespace-padded key. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3 --- backend/windmill-common/src/guest_jwt.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/backend/windmill-common/src/guest_jwt.rs b/backend/windmill-common/src/guest_jwt.rs index df356ddba2..9c03fb3940 100644 --- a/backend/windmill-common/src/guest_jwt.rs +++ b/backend/windmill-common/src/guest_jwt.rs @@ -82,15 +82,16 @@ pub async fn key_source(db: &DB, w_id: &str) -> Result pub fn decoding_key_from_pem(pem: &str) -> Result<(DecodingKey, &'static [Algorithm])> { use base64::{engine::general_purpose::STANDARD, Engine}; use spki::der::Decode; - let pem = pem.trim(); // The key is admin-set into an unbounded `TEXT` column and reparsed on every guest-JWT // request; a well-formed key with an oversized modulus would pass the checks below. Refuse - // one larger than any real public key before decoding or storing it. + // one larger than any real public key before decoding or storing it. Measure the untrimmed + // input: the endpoint stores what the admin sent, so whitespace padding counts too. if pem.len() > MAX_GUEST_PEM_LEN { return Err(Error::BadRequest(format!( "guest key is longer than {MAX_GUEST_PEM_LEN} bytes" ))); } + let pem = pem.trim(); // A verification key must be public. jsonwebtoken 8.3 keys the public/private distinction // off the PEM label alone and never inspects the DER, so private material relabelled // `PUBLIC KEY` would be stored and then served back through the settings response. Decode @@ -617,6 +618,11 @@ mod tests { ); let err = decoding_key_from_pem(&big).err().unwrap().to_string(); assert!(err.contains("longer than"), "{err}"); + // Whitespace padding must count: the endpoint stores the untrimmed value, so the cap is + // measured before trimming rather than on the small trimmed key it would otherwise see. + let padded = format!("{}{RSA_PUBLIC}", " ".repeat(MAX_GUEST_PEM_LEN)); + let err = decoding_key_from_pem(&padded).err().unwrap().to_string(); + assert!(err.contains("longer than"), "{err}"); } #[test]