diff --git a/apps/studio/components/interfaces/Home/SecurityStatus.tsx b/apps/studio/components/interfaces/Home/SecurityStatus.tsx index be4dc9b4425..7bf2277aef9 100644 --- a/apps/studio/components/interfaces/Home/SecurityStatus.tsx +++ b/apps/studio/components/interfaces/Home/SecurityStatus.tsx @@ -1,9 +1,10 @@ -import { CheckCircle2, ChevronRight, Loader2 } from 'lucide-react' +import { CheckCircle2, ChevronRight, InfoIcon, Loader2 } from 'lucide-react' import Link from 'next/link' import { Fragment, useState } from 'react' import { useParams } from 'common' import { useProjectLintsQuery } from 'data/lint/lint-query' +import { useServiceRoleKeyLeakQuery } from 'data/lint/service-role-key-leak-query' import { Button, PopoverContent_Shadcn_, @@ -27,6 +28,10 @@ export const SecurityStatus = () => { const totalLints = errorLints.length + warnLints.length + infoLints.length const noIssuesFound = totalLints === 0 + const { data: hasServiceRoleKeyLeak } = useServiceRoleKeyLeakQuery({ + projectRef: ref, + }) + return ( @@ -73,6 +78,31 @@ export const SecurityStatus = () => { {totalLints} issue{totalLints > 1 ? 's' : ''} have been identified

+ {hasServiceRoleKeyLeak && ( +
+ +
+
+
+ +
+
+

Service Role Key Leak

+

+ Your service role key may be compromised. Rotate this key immediately. +

+
+
+ +
+ + +
+ )} + {[ { lints: errorLints, level: LINTER_LEVELS.ERROR }, { lints: warnLints, level: LINTER_LEVELS.WARN }, diff --git a/apps/studio/components/interfaces/Settings/API/JWTSettings.tsx b/apps/studio/components/interfaces/Settings/API/JWTSettings.tsx index deafd573c5e..29ec63b7c9e 100644 --- a/apps/studio/components/interfaces/Settings/API/JWTSettings.tsx +++ b/apps/studio/components/interfaces/Settings/API/JWTSettings.tsx @@ -226,6 +226,11 @@ const JWTSettings = () => {

A random secret will be created, or you can create your own.

+

+ This will invalidate all existing API keys, including your{' '} + service_role and{' '} + anon keys. +

{isUpdatingJwtSecret ? ( @@ -244,13 +249,13 @@ const JWTSettings = () => { }, }} > - Generate a new secret + Generate new API Keys ) : ( @@ -259,7 +264,7 @@ const JWTSettings = () => { onClick={() => setIsGeneratingKey(true)} > -

Generate a random secret

+

Generate new API Keys

+ {hasServiceRoleKeyLeak && x.tags === 'service_role' && ( + + + Urgent: Service Role key leak detected + +

+ Your service role key may be compromised. Rotate it immediately and ensure + it's used only on a private server. We detected that it is likely being used + on the client-side, in a browser or mobile app.{' '} + Generate new API keys below in JWT Settings + to rotate this key. +

+

+

+ Read{' '} + + Understanding API Keys + {' '} + to learn more. +

+
+
+ )} )) )} @@ -161,7 +191,7 @@ const DisplayApiSettings = ({ ) : ( ['projects', projectRef, 'lint'] as const, + serviceRoleKeyLeak: (projectRef: string | undefined) => + ['projects', projectRef, 'service-role-key-leak'] as const, } diff --git a/apps/studio/data/lint/service-role-key-leak-query.ts b/apps/studio/data/lint/service-role-key-leak-query.ts new file mode 100644 index 00000000000..2e1bb463753 --- /dev/null +++ b/apps/studio/data/lint/service-role-key-leak-query.ts @@ -0,0 +1,56 @@ +import { UseQueryOptions, useQuery } from '@tanstack/react-query' + +import { useSelectedProject } from 'hooks/misc/useSelectedProject' +import { BASE_PATH, PROJECT_STATUS } from 'lib/constants' +import { ResponseError } from 'types' +import { lintKeys } from './keys' + +export type ServiceRoleKeyLeakVariables = { + projectRef?: string +} + +export type ServiceRoleKeyLeakResponse = boolean + +export async function checkServiceRoleKeyLeak( + { projectRef }: ServiceRoleKeyLeakVariables, + signal?: AbortSignal +): Promise { + if (!projectRef) throw new Error('Project ref is required') + + const res = await fetch(`${BASE_PATH}/api/check-service-role-key-leak`, { + headers: { + 'x-project': projectRef, + }, + signal, + }) + + if (!res.ok) { + const error = await res.json() + throw new Error(error.error?.message || 'Failed to fetch service role key leak data') + } + + return res.json() +} + +export type ServiceRoleKeyLeakData = Awaited> +export type ServiceRoleKeyLeakError = ResponseError + +export const useServiceRoleKeyLeakQuery = ( + { projectRef }: ServiceRoleKeyLeakVariables, + { + enabled = true, + ...options + }: UseQueryOptions = {} +) => { + const project = useSelectedProject() + const isActive = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY + + return useQuery( + lintKeys.serviceRoleKeyLeak(projectRef), + ({ signal }) => checkServiceRoleKeyLeak({ projectRef }, signal), + { + enabled: enabled && typeof projectRef !== 'undefined' && isActive, + ...options, + } + ) +} diff --git a/apps/studio/middleware.ts b/apps/studio/middleware.ts index 8256a9563cd..dd9f0d62f43 100644 --- a/apps/studio/middleware.ts +++ b/apps/studio/middleware.ts @@ -20,6 +20,7 @@ const HOSTED_SUPPORTED_API_URLS = [ '/get-ip-address', '/get-utc-time', '/edge-functions/test', + '/check-service-role-key-leak', ] export function middleware(request: NextRequest) { diff --git a/apps/studio/pages/api/check-service-role-key-leak.ts b/apps/studio/pages/api/check-service-role-key-leak.ts new file mode 100644 index 00000000000..22d3632532f --- /dev/null +++ b/apps/studio/pages/api/check-service-role-key-leak.ts @@ -0,0 +1,54 @@ +import apiWrapper from 'lib/api/apiWrapper' +import { NextApiRequest, NextApiResponse } from 'next' + +async function handler(req: NextApiRequest, res: NextApiResponse) { + const { method } = req + + switch (method) { + case 'GET': + return handleGet(req, res) + default: + res.setHeader('Allow', ['GET']) + res.status(405).json({ data: null, error: { message: `Method ${method} Not Allowed` } }) + } +} + +async function handleGet(req: NextApiRequest, res: NextApiResponse) { + const projectRef = req.headers['x-project'] as string + console.log('the ref', projectRef) + + if (!projectRef) { + return res.status(400).json({ error: 'Project reference is required' }) + } + + try { + const response = await fetch( + `https://api.logflare.app/api/endpoints/query/exposed%20service_role%20advisor%20hack?project=${projectRef}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json; charset=utf-8', + 'X-API-KEY': process.env.LOGFLARE_API_KEY!, + }, + } + ) + + if (!response.ok) { + throw new Error(`Logflare API responded with status ${response.status}`) + } + + const data = await response.json() + + return res.json(data.result.length > 0) + } catch (error) { + console.error('Error fetching service role key leak data:', error) + return res.status(500).json({ + error: + 'There was an unknown error fetching the service role key leak data. Please try again.', + }) + } +} + +const wrapper = (req: NextApiRequest, res: NextApiResponse) => apiWrapper(req, res, handler) + +export default wrapper