fix: don't mark private-mode blobs as publicly cacheable
The immutable Cache-Control added for the reader preload work was `public` unconditionally. Under PRIVATE_MODE, /files is auth-gated, so a shared cache/CDN would store the blob and serve it to unauthenticated clients, defeating the gate. Emit `private, ...` when private_mode is on, `public, ...` otherwise. Found in the security audit; regression from 0.124.3. Bump to 0.124.9. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -149,6 +149,56 @@ async fn private_mode_blocks_register_even_when_self_register_enabled(pool: PgPo
|
||||
assert_eq!(body["error"]["code"], "forbidden");
|
||||
}
|
||||
|
||||
#[sqlx::test(migrations = "./migrations")]
|
||||
async fn private_mode_serves_files_as_private_not_public_cacheable(pool: PgPool) {
|
||||
// In private mode /files is auth-gated, so a served blob must NOT carry a
|
||||
// `public` Cache-Control: a shared cache / CDN would otherwise store it and
|
||||
// hand it to anonymous clients, defeating the gate. It should be `private`.
|
||||
//
|
||||
// Register via a public harness on the shared pool so the session exists,
|
||||
// then upload + fetch a cover through the private harness (same storage).
|
||||
let public = common::harness(pool.clone());
|
||||
let (_, cookie) = common::register_user(&public.app).await;
|
||||
|
||||
let private = common::harness_with_private_mode(pool);
|
||||
let form = common::MultipartBuilder::new()
|
||||
.add_json("metadata", json!({ "title": "Secret Library" }))
|
||||
.add_file("cover", "cover.png", "image/png", &common::fake_png_bytes());
|
||||
let created = private
|
||||
.app
|
||||
.clone()
|
||||
.oneshot(common::post_multipart_with_cookie(
|
||||
"/api/v1/mangas",
|
||||
form,
|
||||
&cookie,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(created.status(), StatusCode::CREATED);
|
||||
let body = common::body_json(created).await;
|
||||
let key = body["cover_image_path"].as_str().unwrap().to_string();
|
||||
|
||||
let resp = private
|
||||
.app
|
||||
.oneshot(common::get_with_cookie(
|
||||
&format!("/api/v1/files/{key}"),
|
||||
&cookie,
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let cc = resp
|
||||
.headers()
|
||||
.get(axum::http::header::CACHE_CONTROL)
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap();
|
||||
assert!(
|
||||
cc.starts_with("private") && !cc.contains("public"),
|
||||
"private-mode blobs must be `private`, not `public`; got: {cc}"
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test(migrations = "./migrations")]
|
||||
async fn auth_config_reports_private_mode_and_effective_self_register(pool: PgPool) {
|
||||
let h = common::harness_with_private_mode(pool);
|
||||
|
||||
Reference in New Issue
Block a user