mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-06 16:02:23 +00:00
fix: bound the guest JWT length before verifying or caching
The auth cache keys on the bearer token, and verify_for_workspace decoded a token of any length (its header for the JWKS kid, then the body) before rejecting it, so an oversized token could be decoded unauthenticated and, if it verified, cached at full size. Refuse a token longer than MAX_GUEST_JWT_LEN (8 KiB) at the top of verify_for_workspace, before the key lookup or any signature work. Also correct the MAX_JWKS_URL_LEN doc: the bound holds because the save path validates the URL through fetch_jwks, not because cached_jwks checks it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VF3v6LA9399gNphmZaHYG3
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
1751fb6920
commit
0937fb66ca
@@ -305,6 +305,9 @@ async fn guest_jwt_refusals(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
c.app_path = "u/test-user/*".to_string();
|
||||
let wildcard_app_path = bearer(&c, PRIV1, Algorithm::ES256);
|
||||
|
||||
// a token past the length cap is refused before any signature work or caching.
|
||||
let oversized_token = format!("jwt_guest_{}", "a".repeat(9000));
|
||||
|
||||
for (label, token) in [
|
||||
("wrong workspace", wrong_ws),
|
||||
("wrong key", wrong_key),
|
||||
@@ -317,6 +320,7 @@ async fn guest_jwt_refusals(db: Pool<Postgres>) -> anyhow::Result<()> {
|
||||
("group-shaped email", group_shaped_email),
|
||||
("oversized email", oversized_email),
|
||||
("wildcard app_path", wildcard_app_path),
|
||||
("oversized token", oversized_token),
|
||||
] {
|
||||
let resp = whoami(port, ws, &token).send().await?;
|
||||
assert_eq!(resp.status(), 401, "{label} must be refused");
|
||||
|
||||
@@ -261,8 +261,9 @@ const JWKS_MAX_RETAINED_BYTES: usize = 64 * 1024;
|
||||
const JWKS_MAX_KEYS: usize = 50;
|
||||
|
||||
/// Both JWKS caches key on the admin-supplied URL string. The column is unbounded `TEXT`, so
|
||||
/// without this a workspace admin rotating long URLs could grow the caches by the URL bytes
|
||||
/// alone. A real JWKS URL is well under this; the check runs before the URL is ever cached.
|
||||
/// without this a workspace admin could grow the caches by the URL bytes alone. Enforced in
|
||||
/// `fetch_jwks`, which `edit_guest_jwt_key` validates through, so an overlong URL is never
|
||||
/// stored; the cache only ever sees a URL that was stored, hence a bounded one.
|
||||
const MAX_JWKS_URL_LEN: usize = 2048;
|
||||
|
||||
/// A guest verification key is admin-set into an unbounded `TEXT` column and reparsed on every
|
||||
@@ -270,6 +271,11 @@ const MAX_JWKS_URL_LEN: usize = 2048;
|
||||
/// this bounds the stored and reparsed bytes without refusing any real key.
|
||||
const MAX_GUEST_PEM_LEN: usize = 8 * 1024;
|
||||
|
||||
/// A guest JWT is refused past this before any signature work or caching: the auth cache keys
|
||||
/// on the bearer, so an oversized token (unauthenticated at this point) would otherwise be
|
||||
/// decoded and, if it verified, cached at its full size. A real JWT is well under this.
|
||||
const MAX_GUEST_JWT_LEN: usize = 8 * 1024;
|
||||
|
||||
/// Fetch a JWKS, keeping only the keys usable here. The URL was set by a workspace
|
||||
/// admin, so it is validated against private ranges and the connect is pinned to the
|
||||
/// validated addresses; redirects are not followed for the same reason. The body is
|
||||
@@ -483,6 +489,11 @@ pub async fn jwks_key_for(url: &str, token: &str) -> Result<(DecodingKey, Vec<Al
|
||||
/// Verify `token` for `w_id` against whatever key the workspace configured. A PEM key
|
||||
/// ignores `kid`; a JWKS selects by it.
|
||||
pub async fn verify_for_workspace(db: &DB, w_id: &str, token: &str) -> Result<GuestJwtClaims> {
|
||||
if token.len() > MAX_GUEST_JWT_LEN {
|
||||
return Err(Error::NotAuthorized(format!(
|
||||
"guest JWT refused: token is longer than {MAX_GUEST_JWT_LEN} bytes"
|
||||
)));
|
||||
}
|
||||
let Some(source) = key_source(db, w_id).await? else {
|
||||
return Err(Error::NotAuthorized(format!(
|
||||
"guest JWT refused: workspace {w_id} has no guest JWT key"
|
||||
|
||||
Reference in New Issue
Block a user