diff options
| author | nsfisis <nsfisis@gmail.com> | 2025-12-30 22:08:47 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2025-12-30 22:08:47 +0900 |
| commit | c2eb7513834eeb5adfa53fff897f585de87e4821 (patch) | |
| tree | 9e914051ca67e2f9e1fa301119bdec398ec9e55f /src/server/middleware/cors.test.ts | |
| parent | b839cae49efd4b9d35c2868a4137101a4d71bd7f (diff) | |
| download | kioku-c2eb7513834eeb5adfa53fff897f585de87e4821.tar.gz kioku-c2eb7513834eeb5adfa53fff897f585de87e4821.tar.zst kioku-c2eb7513834eeb5adfa53fff897f585de87e4821.zip | |
feat(security): add rate limiting and CORS middleware
- Add rate limiting to login endpoint (5 requests/minute per IP)
- Configure CORS middleware with environment-based origin control
- Expose rate limit headers in CORS for client visibility
- Update hono to 4.11.3 for rate limiter peer dependency
🤖 Generated with [Claude Code](https://claude.ai/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Diffstat (limited to 'src/server/middleware/cors.test.ts')
| -rw-r--r-- | src/server/middleware/cors.test.ts | 169 |
1 files changed, 169 insertions, 0 deletions
diff --git a/src/server/middleware/cors.test.ts b/src/server/middleware/cors.test.ts new file mode 100644 index 0000000..6f413a5 --- /dev/null +++ b/src/server/middleware/cors.test.ts @@ -0,0 +1,169 @@ +import { Hono } from "hono"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { createCorsMiddleware } from "./cors.js"; + +describe("createCorsMiddleware", () => { + const originalEnv = process.env.CORS_ORIGIN; + + afterEach(() => { + if (originalEnv === undefined) { + delete process.env.CORS_ORIGIN; + } else { + process.env.CORS_ORIGIN = originalEnv; + } + }); + + describe("when CORS_ORIGIN is not set", () => { + beforeEach(() => { + delete process.env.CORS_ORIGIN; + }); + + it("does not add CORS headers", async () => { + const app = new Hono(); + app.use("*", createCorsMiddleware()); + app.get("/test", (c) => c.json({ ok: true })); + + const res = await app.request("/test", { + headers: { Origin: "https://attacker.com" }, + }); + + expect(res.status).toBe(200); + expect(res.headers.get("Access-Control-Allow-Origin")).toBeNull(); + }); + + it("does not allow preflight requests", async () => { + const app = new Hono(); + app.use("*", createCorsMiddleware()); + app.get("/test", (c) => c.json({ ok: true })); + + const res = await app.request("/test", { + method: "OPTIONS", + headers: { + Origin: "https://attacker.com", + "Access-Control-Request-Method": "POST", + }, + }); + + expect(res.headers.get("Access-Control-Allow-Origin")).toBeNull(); + }); + }); + + describe("when CORS_ORIGIN is set to single origin", () => { + beforeEach(() => { + process.env.CORS_ORIGIN = "https://allowed.example.com"; + }); + + it("allows requests from the configured origin", async () => { + const app = new Hono(); + app.use("*", createCorsMiddleware()); + app.get("/test", (c) => c.json({ ok: true })); + + const res = await app.request("/test", { + headers: { Origin: "https://allowed.example.com" }, + }); + + expect(res.status).toBe(200); + expect(res.headers.get("Access-Control-Allow-Origin")).toBe( + "https://allowed.example.com", + ); + }); + + it("does not allow requests from other origins", async () => { + const app = new Hono(); + app.use("*", createCorsMiddleware()); + app.get("/test", (c) => c.json({ ok: true })); + + const res = await app.request("/test", { + headers: { Origin: "https://attacker.com" }, + }); + + expect(res.status).toBe(200); + expect(res.headers.get("Access-Control-Allow-Origin")).toBeNull(); + }); + + it("handles preflight requests correctly", async () => { + const app = new Hono(); + app.use("*", createCorsMiddleware()); + app.post("/test", (c) => c.json({ ok: true })); + + const res = await app.request("/test", { + method: "OPTIONS", + headers: { + Origin: "https://allowed.example.com", + "Access-Control-Request-Method": "POST", + }, + }); + + expect(res.headers.get("Access-Control-Allow-Origin")).toBe( + "https://allowed.example.com", + ); + expect(res.headers.get("Access-Control-Allow-Methods")).toContain("POST"); + }); + + it("exposes rate limit headers", async () => { + const app = new Hono(); + app.use("*", createCorsMiddleware()); + app.get("/test", (c) => c.json({ ok: true })); + + const res = await app.request("/test", { + method: "OPTIONS", + headers: { + Origin: "https://allowed.example.com", + "Access-Control-Request-Method": "GET", + }, + }); + + const exposeHeaders = res.headers.get("Access-Control-Expose-Headers"); + expect(exposeHeaders).toContain("RateLimit-Limit"); + expect(exposeHeaders).toContain("RateLimit-Remaining"); + expect(exposeHeaders).toContain("RateLimit-Reset"); + }); + }); + + describe("when CORS_ORIGIN is set to multiple origins", () => { + beforeEach(() => { + process.env.CORS_ORIGIN = + "https://app.example.com, https://admin.example.com"; + }); + + it("allows requests from first configured origin", async () => { + const app = new Hono(); + app.use("*", createCorsMiddleware()); + app.get("/test", (c) => c.json({ ok: true })); + + const res = await app.request("/test", { + headers: { Origin: "https://app.example.com" }, + }); + + expect(res.headers.get("Access-Control-Allow-Origin")).toBe( + "https://app.example.com", + ); + }); + + it("allows requests from second configured origin", async () => { + const app = new Hono(); + app.use("*", createCorsMiddleware()); + app.get("/test", (c) => c.json({ ok: true })); + + const res = await app.request("/test", { + headers: { Origin: "https://admin.example.com" }, + }); + + expect(res.headers.get("Access-Control-Allow-Origin")).toBe( + "https://admin.example.com", + ); + }); + + it("does not allow requests from unlisted origins", async () => { + const app = new Hono(); + app.use("*", createCorsMiddleware()); + app.get("/test", (c) => c.json({ ok: true })); + + const res = await app.request("/test", { + headers: { Origin: "https://other.example.com" }, + }); + + expect(res.headers.get("Access-Control-Allow-Origin")).toBeNull(); + }); + }); +}); |
