fix(security): harden auth reset, rate limiter fallback, and CI secrets
- Move CI_AUTH_SECRET from plaintext to ${{ secrets.CI_AUTH_SECRET }}
- Wrap password reset (update + session kill + token mark) in $transaction
to prevent stale sessions on partial failure (CWE-613)
- Rate limiter Redis fallback now uses stricter degraded limits
(maxRequests/10) and logs at error level instead of warn
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -26,7 +26,7 @@ function makeDb(
|
||||
resetToken?: Partial<Record<string, unknown>>;
|
||||
} = {},
|
||||
) {
|
||||
return {
|
||||
const db = {
|
||||
user: {
|
||||
findUnique: vi.fn().mockResolvedValue({ id: "user_1", email: "user@example.com" }),
|
||||
update: vi.fn().mockResolvedValue({ id: "user_1" }),
|
||||
@@ -42,7 +42,9 @@ function makeDb(
|
||||
update: vi.fn().mockResolvedValue({}),
|
||||
...overrides.resetToken,
|
||||
},
|
||||
$transaction: vi.fn(async (fn: (tx: unknown) => Promise<unknown>) => fn(db)),
|
||||
} as never;
|
||||
return db;
|
||||
}
|
||||
|
||||
function makeCtx(db = makeDb()) {
|
||||
|
||||
@@ -47,18 +47,20 @@ describe("rate limiter", () => {
|
||||
vi.doMock("ioredis", () => ({
|
||||
Redis: vi.fn().mockImplementation(() => ({
|
||||
on: vi.fn(),
|
||||
eval: vi.fn(async (_script: string, _numKeys: number, key: string, windowMsValue: string) => {
|
||||
const now = Date.now();
|
||||
const windowMs = Number(windowMsValue);
|
||||
const existing = store.get(key);
|
||||
if (!existing || existing.resetAt <= now) {
|
||||
store.set(key, { count: 1, resetAt: now + windowMs });
|
||||
} else {
|
||||
existing.count += 1;
|
||||
}
|
||||
const current = store.get(key)!;
|
||||
return [current.count, current.resetAt - now];
|
||||
}),
|
||||
eval: vi.fn(
|
||||
async (_script: string, _numKeys: number, key: string, windowMsValue: string) => {
|
||||
const now = Date.now();
|
||||
const windowMs = Number(windowMsValue);
|
||||
const existing = store.get(key);
|
||||
if (!existing || existing.resetAt <= now) {
|
||||
store.set(key, { count: 1, resetAt: now + windowMs });
|
||||
} else {
|
||||
existing.count += 1;
|
||||
}
|
||||
const current = store.get(key)!;
|
||||
return [current.count, current.resetAt - now];
|
||||
},
|
||||
),
|
||||
scan: vi.fn(async () => ["0", [...store.keys()]]),
|
||||
del: delMock,
|
||||
})),
|
||||
@@ -88,7 +90,7 @@ describe("rate limiter", () => {
|
||||
expect(afterReset.remaining).toBe(1);
|
||||
});
|
||||
|
||||
it("falls back to in-memory counters when Redis is unavailable", async () => {
|
||||
it("falls back to degraded in-memory counters when Redis is unavailable", async () => {
|
||||
vi.doMock("ioredis", () => ({
|
||||
Redis: vi.fn().mockImplementation(() => ({
|
||||
on: vi.fn(),
|
||||
@@ -101,7 +103,9 @@ describe("rate limiter", () => {
|
||||
}));
|
||||
|
||||
const { createRateLimiter } = await import("../middleware/rate-limit.js");
|
||||
const limiter = createRateLimiter(60_000, 2, {
|
||||
// Degraded fallback uses max(1, floor(maxRequests/10)), so with
|
||||
// maxRequests=20 the degraded limit is 2.
|
||||
const limiter = createRateLimiter(60_000, 20, {
|
||||
backend: "redis",
|
||||
redisUrl: "redis://test",
|
||||
name: "redis-fallback-test",
|
||||
|
||||
Reference in New Issue
Block a user