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:
@@ -886,4 +886,64 @@ mod tests {
|
||||
s.update(&cx, "users", id, json!({ "x": 2 })).await.unwrap();
|
||||
let _ = s.delete(&cx, "users", id).await.unwrap();
|
||||
}
|
||||
|
||||
/// `PICLOUD_DOCS_MAX_VALUE_BYTES` — the docs analogue of the KV anti-DoS rail.
|
||||
/// Pins that an oversized document is rejected BEFORE authz, and covers create
|
||||
/// AND update (both encode-and-check). The cx is an authenticated member with
|
||||
/// no role (authz-denied) so the ordering is observable: size-first returns
|
||||
/// `ValueTooLarge`, authz-first would return `Forbidden`. An anon cx couldn't
|
||||
/// tell the two apart.
|
||||
#[tokio::test]
|
||||
async fn oversized_document_is_rejected_before_authz() {
|
||||
let s = DocsServiceImpl::with_max_value_bytes(
|
||||
Arc::new(InMemoryDocsRepo::default()),
|
||||
Arc::new(DenyingAuthzRepo),
|
||||
Arc::new(NoopEventEmitter),
|
||||
16,
|
||||
);
|
||||
let cx = member_no_role_cx(AppId::new());
|
||||
|
||||
let err = s
|
||||
.create(&cx, "users", json!({ "blob": "x".repeat(100) }))
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(err, DocsError::ValueTooLarge { limit: 16, .. }),
|
||||
"an oversized create must be ValueTooLarge before authz; got {err:?}"
|
||||
);
|
||||
|
||||
// Control: an under-cap create with the same cx is Forbidden — the cx is
|
||||
// genuinely authz-denied, so the case above bypassed authz. And it seeds a
|
||||
// doc via an ALLOWING service so we can then test the update path.
|
||||
assert!(
|
||||
matches!(
|
||||
s.create(&cx, "users", json!({ "x": 1 })).await.unwrap_err(),
|
||||
DocsError::Forbidden
|
||||
),
|
||||
"the control confirms this cx is authz-denied"
|
||||
);
|
||||
|
||||
// Update also encodes-and-checks before authz — an update that skipped the
|
||||
// cap would be a free bypass. Seed with an allowing service, then update
|
||||
// through the denied one.
|
||||
let seeder = DocsServiceImpl::with_max_value_bytes(
|
||||
Arc::new(InMemoryDocsRepo::default()),
|
||||
Arc::new(AllowingAuthzRepo),
|
||||
Arc::new(NoopEventEmitter),
|
||||
16,
|
||||
);
|
||||
let owner = owner_cx(AppId::new());
|
||||
let id = seeder
|
||||
.create(&owner, "users", json!({ "x": 1 }))
|
||||
.await
|
||||
.unwrap();
|
||||
let err = seeder
|
||||
.update(&owner, "users", id, json!({ "blob": "x".repeat(100) }))
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(err, DocsError::ValueTooLarge { limit: 16, .. }),
|
||||
"an oversized update must be ValueTooLarge too; got {err:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user