From 2b66513fcb7fce803906866c6bb97235d91126a6 Mon Sep 17 00:00:00 2001 From: All Asmaul Husnain <2263081025@uttarauniversity.edu.bd> Date: Sat, 24 Oct 2026 14:50:04 +0600 Subject: [PATCH 1/5] fix: disable caching for authentication and secret responses --- app/main.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/main.py b/app/main.py index 3500830..d2ca4a2 100644 --- a/app/main.py +++ b/app/main.py @@ -35,6 +35,17 @@ async def lifespan(_: FastAPI) -> AsyncIterator[None]: app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) app.add_middleware(SlowAPIMiddleware) + + +@app.middleware("http") +async def prevent_sensitive_response_caching(request: Request, call_next): + response = await call_next(request) + path = request.url.path + if path.startswith("/auth/") or path == "/secrets" or path.startswith("/secrets/"): + response.headers["Cache-Control"] = "no-store" + return response + + bearer_scheme = HTTPBearer(auto_error=False) Session = Annotated[AsyncSession, Depends(get_session)] # Keep the password verification cost similar for unknown and existing accounts. From cc6795a9164b24ed42ff1cd2eb0fe0ee6f9ef0ad Mon Sep 17 00:00:00 2001 From: All Asmaul Husnain <2263081025@uttarauniversity.edu.bd> Date: Sat, 24 Oct 2026 14:50:09 +0600 Subject: [PATCH 2/5] test: prevent caching authentication responses --- tests/test_auth.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_auth.py b/tests/test_auth.py index 498ad0f..d94b76b 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -30,7 +30,9 @@ def test_login_rejects_invalid_password() -> None: ) assert registered.status_code == 201 + assert registered.headers["cache-control"] == "no-store" assert invalid_login.status_code == 401 + assert invalid_login.headers["cache-control"] == "no-store" assert invalid_login.json()["detail"] == "Invalid credentials" From c88bb15a3ef994cc47d91e4c45dbe67edbb91fdf Mon Sep 17 00:00:00 2001 From: All Asmaul Husnain <2263081025@uttarauniversity.edu.bd> Date: Sat, 24 Oct 2026 14:50:15 +0600 Subject: [PATCH 3/5] test: prevent caching secret responses --- tests/test_secrets.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_secrets.py b/tests/test_secrets.py index 8edc9e0..dd6ce5e 100644 --- a/tests/test_secrets.py +++ b/tests/test_secrets.py @@ -39,6 +39,7 @@ def test_secret_lifecycle_and_user_isolation() -> None: json={"name": "database", "value": "initial-secret"}, ) assert created.status_code == 201 + assert created.headers["cache-control"] == "no-store" secret = created.json() secret_id = secret["id"] assert secret["name"] == "database" @@ -46,10 +47,12 @@ def test_secret_lifecycle_and_user_isolation() -> None: listed = client.get("/secrets", headers=owner_headers) assert listed.status_code == 200 + assert listed.headers["cache-control"] == "no-store" assert [item["id"] for item in listed.json()] == [secret_id] other_access = client.get(f"/secrets/{secret_id}", headers=other_headers) assert other_access.status_code == 404 + assert other_access.headers["cache-control"] == "no-store" updated = client.patch( f"/secrets/{secret_id}", @@ -57,15 +60,18 @@ def test_secret_lifecycle_and_user_isolation() -> None: json={"name": "database-prod", "value": "updated-secret"}, ) assert updated.status_code == 200 + assert updated.headers["cache-control"] == "no-store" assert updated.json()["name"] == "database-prod" assert updated.json()["value"] == "updated-secret" fetched = client.get(f"/secrets/{secret_id}", headers=owner_headers) assert fetched.status_code == 200 + assert fetched.headers["cache-control"] == "no-store" assert fetched.json()["value"] == "updated-secret" deleted = client.delete(f"/secrets/{secret_id}", headers=owner_headers) assert deleted.status_code == 204 + assert deleted.headers["cache-control"] == "no-store" missing = client.get(f"/secrets/{secret_id}", headers=owner_headers) assert missing.status_code == 404 From e1c657fd9948cbd810c138ac940e6e053fe33449 Mon Sep 17 00:00:00 2001 From: All Asmaul Husnain <2263081025@uttarauniversity.edu.bd> Date: Sat, 24 Oct 2026 14:50:34 +0600 Subject: [PATCH 4/5] docs: document no-store response policy --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0f767f0..3cb46ef 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,8 @@ Passwords are hashed with Argon2 rather than stored as plain text. Secret values The API decrypts a secret when returning it to its authenticated owner. This is server-side encryption, not end-to-end encryption. +Authentication and secret-management responses include `Cache-Control: no-store` so clients and intermediaries are instructed not to cache access tokens or decrypted secret values. + Successful secret creation, listing, retrieval, updates, and deletion are recorded in `audit_events` with the action, user ID, secret ID when applicable, and timestamp. Secret values are not included in these events. Authentication attempts are not currently audited. The Docker Compose configuration contains credentials and an encryption key intended only for local development. Do not reuse these values in a deployed environment. Use unique secrets supplied through an appropriate secret manager or deployment configuration. From 400f88520c6dd94518409737f52fd7caff174628 Mon Sep 17 00:00:00 2001 From: All Asmaul Husnain <2263081025@uttarauniversity.edu.bd> Date: Sat, 24 Oct 2026 14:53:20 +0600 Subject: [PATCH 5/5] style: satisfy Ruff middleware condition --- app/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/main.py b/app/main.py index d2ca4a2..432ed43 100644 --- a/app/main.py +++ b/app/main.py @@ -41,7 +41,7 @@ async def lifespan(_: FastAPI) -> AsyncIterator[None]: async def prevent_sensitive_response_caching(request: Request, call_next): response = await call_next(request) path = request.url.path - if path.startswith("/auth/") or path == "/secrets" or path.startswith("/secrets/"): + if path == "/secrets" or path.startswith(("/auth/", "/secrets/")): response.headers["Cache-Control"] = "no-store" return response