mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
chore(release): v1.28.48 — security follow-ups
From the campaign's security re-verification pass (posture otherwise held
across the 10-release week — no Critical/High):
- MEDIUM: the OIDC auto-provision branch re-introduced the first-admin race
the password-register route fixed in v1.28.42 — count-then-create with no
lock, so two concurrent OIDC first-logins (or an OIDC first-login racing a
password first-registration) in the empty-DB window could both mint ADMIN.
Now wraps count+role+create in a $transaction taking the SAME advisory lock
key ('register:first-admin') and re-counts inside, so the two paths
serialise; every provisioned field preserved.
- LOW: the nutrients batch catch echoed the raw exception message into the
client-facing reason (violating its closed reason-set). Now returns the
fixed 'upsert_failed' member and logs the detail server-side only.
No migration (advisory lock is runtime).
This commit is contained in:
1 parent
f3a3b8f3a7
commit
a9a318d4a4
8 files changed
+163
-20
No files matched your search
@@ -333,9 +333,12 @@ describe("POST /api/nutrients/batch — upsert semantics", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("a thrown upsert marks only that entry skipped upsert_failed", async () => {
|
||||
it("a thrown upsert marks only that entry skipped upsert_failed without leaking the raw error", async () => {
|
||||
// The exception message must never reach the client — the per-entry
|
||||
// `reason` is a closed set. A raw-message echo was a v1.28.48 fix.
|
||||
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
vi.mocked(prisma.nutrientIntakeDay.upsert)
|
||||
.mockRejectedValueOnce(new Error("boom"))
|
||||
.mockRejectedValueOnce(new Error("boom: sensitive db detail"))
|
||||
.mockResolvedValueOnce({} as never);
|
||||
|
||||
const res = await POST(
|
||||
@@ -348,7 +351,13 @@ describe("POST /api/nutrients/batch — upsert semantics", () => {
|
||||
);
|
||||
const body = (await res.json()) as BatchResponse;
|
||||
expect(body.data.entries[0].status).toBe("skipped");
|
||||
expect(body.data.entries[0].reason).toBe("upsert_failed");
|
||||
expect(body.data.skipped[0].reason).toBe("upsert_failed");
|
||||
// The raw exception text is logged server-side only, never echoed.
|
||||
expect(JSON.stringify(body)).not.toContain("sensitive db detail");
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(body.data.entries[1].status).toBe("inserted");
|
||||
expect(body.data.inserted).toBe(1);
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -208,8 +208,12 @@ async function postBatch(request: NextRequest): Promise<Response> {
|
||||
results.push({ index: i, status: "inserted" });
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const reason =
|
||||
err instanceof Error ? err.message.slice(0, 120) : "upsert_failed";
|
||||
// The client-facing `reason` is a closed set (see the file header) —
|
||||
// never echo raw exception text into it. Log the real error
|
||||
// server-side only (SWC keeps `console.error` in prod) and return the
|
||||
// fixed `upsert_failed` member.
|
||||
console.error("[nutrient-batch] upsert failed", err);
|
||||
const reason = "upsert_failed";
|
||||
skipped.push({ index: i, reason });
|
||||
results.push({ index: i, status: "skipped", reason });
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user