test(services): pin that the per-value byte caps reject before authz
CLAUDE.md calls the KV/docs/pubsub/queue value-size caps an anti-DoS rail: oversized payloads are rejected "before authz so anonymous public scripts can't DoS Postgres." Only queue had a test, and it used an allow-all authz + anon cx — which catches a DROPPED cap but not a REORDERED one, because an anon cx passes script_gate regardless. Each service now has an ordering-proof test: a DENYING authz repo + an AUTHENTICATED member cx, so a size-check-first service returns *TooLarge while an authz-first one would return Forbidden. Each pairs it with an under-cap control through the same denied cx (returns Forbidden) to prove the cx really is denied, so the *TooLarge case genuinely bypassed authz. Queue's pre-existing test is upgraded to the same shape (+ a shared member_cx helper). Mutation-verified: moving the KV size check after authz flips its result to Forbidden and the test fails. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -561,6 +561,48 @@ mod tests {
|
||||
// No panic, no Forbidden.
|
||||
}
|
||||
|
||||
/// `PICLOUD_KV_MAX_VALUE_BYTES` is called out in CLAUDE.md as an anti-DoS rail:
|
||||
/// oversized values are rejected "before authz so anonymous public scripts
|
||||
/// can't DoS Postgres JSONB columns." Nothing asserted it. This does, and it
|
||||
/// pins the ORDERING — the security-relevant part.
|
||||
///
|
||||
/// The cx is an AUTHENTICATED member with no role, which `DenyingAuthzRepo`
|
||||
/// rejects (see `authed_cx_with_no_role_is_forbidden`). That is what makes the
|
||||
/// ordering observable: an anon cx passes `script_gate` regardless, so it
|
||||
/// could not tell the two orders apart. With a denied cx, a size-check-first
|
||||
/// service returns `ValueTooLarge` and an authz-first one returns `Forbidden`.
|
||||
#[tokio::test]
|
||||
async fn oversized_value_is_rejected_before_authz() {
|
||||
let kv = KvServiceImpl::with_max_value_bytes(
|
||||
Arc::new(InMemoryKvRepo::default()),
|
||||
Arc::new(DenyingAuthzRepo),
|
||||
Arc::new(NoopEventEmitter),
|
||||
16,
|
||||
);
|
||||
let cx = member_no_role_cx(AppId::new());
|
||||
|
||||
let err = kv
|
||||
.set(&cx, "widgets", "k", serde_json::json!("x".repeat(100)))
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(err, KvError::ValueTooLarge { limit: 16, .. }),
|
||||
"an oversized value must be rejected as ValueTooLarge BEFORE authz; an \
|
||||
authz-first order would return Forbidden for this denied cx. got {err:?}"
|
||||
);
|
||||
|
||||
// Control: an under-cap value with the SAME denied cx returns Forbidden —
|
||||
// proving the cx really is denied, so the case above genuinely bypassed authz.
|
||||
let err = kv
|
||||
.set(&cx, "widgets", "k", serde_json::json!("x"))
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(err, KvError::Forbidden),
|
||||
"the control confirms this cx is authz-denied; got {err:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Authenticated principal with no role on the app: the
|
||||
/// `DenyingAuthzRepo` returns no membership, so the capability
|
||||
/// check denies. Set must surface KvError::Forbidden.
|
||||
|
||||
Reference in New Issue
Block a user