Skip to content

Commit a71d188

Browse files
committed
fix(webapp): version rate-limit cache key and validate cached shape on read
1 parent 08c55e4 commit a71d188

1 file changed

Lines changed: 17 additions & 2 deletions

File tree

apps/webapp/app/services/authorizationRateLimitMiddleware.server.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,19 @@ async function resolveRateLimit(
154154
return { config: parsedOverride.data, identifier } satisfies ResolvedRateLimit;
155155
});
156156

157-
return cacheResult.val ?? { config: defaultLimiter };
157+
// Defensive read: the cache is keyed on a shared Redis namespace, so during a
158+
// deploy an entry could have been written by a server running a different
159+
// code version (a different stored shape). Re-validate here so a stale/foreign
160+
// entry can never reach createLimiterFromConfig with an undefined config and
161+
// throw. The cache key is also versioned (see RedisCacheStore keyPrefix), so
162+
// this is belt-and-suspenders.
163+
const cached = cacheResult.val;
164+
const parsedConfig = RateLimiterConfig.safeParse(cached?.config);
165+
166+
return {
167+
config: parsedConfig.success ? parsedConfig.data : defaultLimiter,
168+
identifier: typeof cached?.identifier === "string" ? cached.identifier : undefined,
169+
};
158170
}
159171

160172
/**
@@ -188,7 +200,10 @@ export function authorizationRateLimitMiddleware({
188200
const memory = createLRUMemoryStore(limiterCache?.maxItems ?? 1000);
189201
const redisCacheStore = new RedisCacheStore({
190202
connection: {
191-
keyPrefix: `cache:${keyPrefix}:rate-limit-cache:`,
203+
// Versioned namespace: the cached value shape is part of this key. Bump
204+
// the version whenever ResolvedRateLimit changes so a rolling deploy never
205+
// reads entries written in a previous shape (and vice versa).
206+
keyPrefix: `cache:${keyPrefix}:rate-limit-cache:v2:`,
192207
...redis,
193208
},
194209
});

0 commit comments

Comments
 (0)