fix: close remaining web api security gaps
This commit is contained in:
@@ -58,6 +58,23 @@ test("InMemoryRateLimiter blocks after the configured request budget is exhauste
|
||||
assert.equal(blocked.retryAfterSeconds, 58);
|
||||
});
|
||||
|
||||
test("InMemoryRateLimiter drops expired buckets during periodic sweeps", () => {
|
||||
const limiter = new InMemoryRateLimiter();
|
||||
const policy = {
|
||||
id: "auth",
|
||||
windowMs: 1_000,
|
||||
maxRequests: 1,
|
||||
};
|
||||
|
||||
assert.deepEqual(limiter.consume(policy, "stale", 1_000), { allowed: true });
|
||||
|
||||
for (let index = 0; index < 99; index += 1) {
|
||||
limiter.consume(policy, `fresh-${index}`, 3_000);
|
||||
}
|
||||
|
||||
assert.deepEqual(limiter.consume(policy, "stale", 3_000), { allowed: true });
|
||||
});
|
||||
|
||||
test("assertTrustedOrigin accepts configured origins and requires browser origin metadata", () => {
|
||||
const request = createMockRequest({
|
||||
headers: {
|
||||
|
||||
@@ -66,8 +66,15 @@ export async function readJsonBody(
|
||||
|
||||
export class InMemoryRateLimiter {
|
||||
private readonly entries = new Map<string, RateLimitEntry>();
|
||||
private sweepCounter = 0;
|
||||
|
||||
consume(policy: RateLimitPolicy, key: string, now = Date.now()): RateLimitDecision {
|
||||
this.sweepCounter += 1;
|
||||
|
||||
if (this.sweepCounter % 100 === 0) {
|
||||
this.deleteExpiredEntries(now);
|
||||
}
|
||||
|
||||
const bucketKey = `${policy.id}:${key}`;
|
||||
const existing = this.entries.get(bucketKey);
|
||||
|
||||
@@ -90,6 +97,14 @@ export class InMemoryRateLimiter {
|
||||
this.entries.set(bucketKey, existing);
|
||||
return { allowed: true };
|
||||
}
|
||||
|
||||
private deleteExpiredEntries(now: number): void {
|
||||
for (const [bucketKey, entry] of this.entries.entries()) {
|
||||
if (entry.resetAt <= now) {
|
||||
this.entries.delete(bucketKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getRateLimitClientIp(request: IncomingMessage): string {
|
||||
|
||||
@@ -70,6 +70,7 @@ const server = createServer(async (request, response) => {
|
||||
}
|
||||
|
||||
if (request.method === "POST" && request.url === "/api/auth/register") {
|
||||
assertTrustedOrigin(request, mutationAllowedOrigins);
|
||||
enforceRateLimit(request, authRateLimitPolicy);
|
||||
const body = await readJsonBody(request, { maxBytes: maxJsonBodyBytes });
|
||||
const payload = readAuthPayload(body);
|
||||
@@ -89,6 +90,7 @@ const server = createServer(async (request, response) => {
|
||||
}
|
||||
|
||||
if (request.method === "POST" && request.url === "/api/auth/login") {
|
||||
assertTrustedOrigin(request, mutationAllowedOrigins);
|
||||
enforceRateLimit(request, authRateLimitPolicy);
|
||||
const body = await readJsonBody(request, { maxBytes: maxJsonBodyBytes });
|
||||
const payload = readAuthPayload(body);
|
||||
@@ -108,6 +110,7 @@ const server = createServer(async (request, response) => {
|
||||
}
|
||||
|
||||
if (request.method === "POST" && request.url === "/api/auth/password-reset/request") {
|
||||
assertTrustedOrigin(request, mutationAllowedOrigins);
|
||||
enforceRateLimit(request, authRateLimitPolicy);
|
||||
const body = await readJsonBody(request, { maxBytes: maxJsonBodyBytes });
|
||||
const payload = readEmailOnlyPayload(body);
|
||||
@@ -137,6 +140,7 @@ const server = createServer(async (request, response) => {
|
||||
}
|
||||
|
||||
if (request.method === "POST" && request.url === "/api/auth/password-reset/confirm") {
|
||||
assertTrustedOrigin(request, mutationAllowedOrigins);
|
||||
enforceRateLimit(request, authRateLimitPolicy);
|
||||
const body = await readJsonBody(request, { maxBytes: maxJsonBodyBytes });
|
||||
const payload = readPasswordResetConfirmPayload(body);
|
||||
|
||||
Reference in New Issue
Block a user