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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
This commit is contained in:
Ruben Fiszel
2026-09-04 16:15:25 +02:00
co-authored by Claude Opus 4.8
parent efc62d8e78
commit 1751fb6920
+8 -2
View File
@@ -82,15 +82,16 @@ pub async fn key_source(db: &DB, w_id: &str) -> Result<Option<GuestJwtKeySource>
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]