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]