diff --git a/src/backend/sync/server/routes.ts b/src/backend/sync/server/routes.ts index 5e5177efe..5e7caff85 100644 --- a/src/backend/sync/server/routes.ts +++ b/src/backend/sync/server/routes.ts @@ -87,12 +87,17 @@ router.get("/v2/info", async (_req: Request, res: Response) => { * description: The desktop session token and the account it belongs to. * 401: * description: Not signed in. + * 403: + * description: API keys cannot link a desktop. */ router.post( "/v2/link", authenticateJWT, async (req: Request, res: Response) => { const userId = userOf(req); + if ((req as AuthenticatedRequest).apiKeyId) { + return res.status(403).json({ error: "API keys cannot link a desktop" }); + } try { const user = await createCurrentUserRepository().findById(userId); if (!user) return res.status(401).json({ error: "User not found" }); diff --git a/src/backend/tests/utils/auth-manager-token.test.ts b/src/backend/tests/utils/auth-manager-token.test.ts index 9b946963d..d0c2e7d1c 100644 --- a/src/backend/tests/utils/auth-manager-token.test.ts +++ b/src/backend/tests/utils/auth-manager-token.test.ts @@ -19,7 +19,10 @@ vi.mock("../../database/db/index.js", () => ({ vi.mock("../../database/repositories/factory.js", () => ({ createCurrentSettingsRepository: () => ({ get: async () => null }), createCurrentSessionRepository: () => ({ - findById: async (id: string) => ({ id }), + findById: async (id: string) => + id === "expired" + ? { id, expiresAt: new Date(Date.now() - 1000).toISOString() } + : { id }, }), createCurrentUserRepository: () => ({}), createCurrentApiKeyRepository: () => ({}), @@ -151,6 +154,15 @@ describe("AuthManager token handling", () => { expect(await authManager.verifyJWTToken(token)).toBeNull(); }); + it("rejects a token whose session has expired", async () => { + const token = jwt.sign( + { userId: "user-1", sessionId: "expired" }, + jwtSecret, + { expiresIn: "1h" }, + ); + expect(await authManager.verifyJWTToken(token)).toBeNull(); + }); + it("still accepts the sessionless pending second-factor token", async () => { const token = jwt.sign({ userId: "user-1", pendingTOTP: true }, jwtSecret, { expiresIn: "10m", diff --git a/src/backend/tests/utils/safe-outbound-fetch.test.ts b/src/backend/tests/utils/safe-outbound-fetch.test.ts index 131d48987..5f3f3a524 100644 --- a/src/backend/tests/utils/safe-outbound-fetch.test.ts +++ b/src/backend/tests/utils/safe-outbound-fetch.test.ts @@ -22,6 +22,7 @@ describe("isBlockedAddress", () => { expect(isBlockedAddress("192.168.1.1")).toBe(true); expect(isBlockedAddress("127.0.0.1")).toBe(true); expect(isBlockedAddress("169.254.1.1")).toBe(true); + expect(isBlockedAddress("192.0.0.170")).toBe(true); expect(isBlockedAddress("100.64.0.1")).toBe(true); }); diff --git a/src/backend/utils/auth-manager.ts b/src/backend/utils/auth-manager.ts index 25e065c1e..5e3ade3fa 100644 --- a/src/backend/utils/auth-manager.ts +++ b/src/backend/utils/auth-manager.ts @@ -414,6 +414,10 @@ class AuthManager { return null; } + if (new Date(sessionRecord.expiresAt).getTime() < Date.now()) { + return null; + } + await this.migrateDataKeyFromPayload(payload); } catch (dbError) { databaseLogger.error( diff --git a/src/backend/utils/safe-outbound-fetch.ts b/src/backend/utils/safe-outbound-fetch.ts index f328c35a7..1d657b2b4 100644 --- a/src/backend/utils/safe-outbound-fetch.ts +++ b/src/backend/utils/safe-outbound-fetch.ts @@ -40,6 +40,7 @@ const blockedIpv4Ranges = [ ["127.0.0.0", 8], ["169.254.0.0", 16], // link-local ["172.16.0.0", 12], + ["192.0.0.0", 24], // IETF protocol assignments ["192.168.0.0", 16], ["198.18.0.0", 15], // benchmarking ["224.0.0.0", 4], // multicast @@ -70,7 +71,7 @@ export function isBlockedAddress(address: string): boolean { } // Extracted so the blocklist decision can be tested directly against a -// fake DNS resolver, instead of only through a real fetch()/Agent call — +// fake DNS resolver, instead of only through a real fetch()/Agent call, // the actual bug here lived entirely in this callback, several layers // below where undici's own "fetch failed" wrapping would otherwise hide it. export function createDnsLookupHook(