Check for leaked keys

This commit is contained in:
Terry Sutton committed 2025-03-03 10:48:22 -03:30
1 parent ae9ff9f633
commit 65fd3598f4
7 files changed
+185 -7

No files matched your search

@@ -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 (
<Popover_Shadcn_ modal={false} open={open} onOpenChange={setOpen}>
<PopoverTrigger_Shadcn_ asChild>
@@ -73,6 +78,31 @@ export const SecurityStatus = () => {
{totalLints} issue{totalLints > 1 ? 's' : ''} have been identified
</p>
<PopoverSeparator_Shadcn_ />
{hasServiceRoleKeyLeak && (
<div key={1}>
<Link href={`/project/${ref}/settings/api`}>
<div className="group flex items-center justify-between w-full px-3 py-3 transition hover:bg-surface-300">
<div className="flex gap-x-3">
<div>
<InfoIcon size={14} className="text-destructive-600" />
</div>
<div>
<p className="text-xs">Service Role Key Leak</p>
<p className="text-xs text-foreground-light">
Your service role key may be compromised. Rotate this key immediately.
</p>
</div>
</div>
<ChevronRight
size={14}
className="transition opacity-0 group-hover:opacity-100 shrink-0"
/>
</div>
</Link>
<PopoverSeparator_Shadcn_ />
</div>
)}
<PopoverSeparator_Shadcn_ />
{[
{ lints: errorLints, level: LINTER_LEVELS.ERROR },
{ lints: warnLints, level: LINTER_LEVELS.WARN },
@@ -226,6 +226,11 @@ const JWTSettings = () => {
<p className="text-sm opacity-50">
A random secret will be created, or you can create your own.
</p>
<p className="text-sm opacity-50">
This will invalidate all existing API keys, including your{' '}
<code className="text-xs">service_role</code> and{' '}
<code className="text-xs">anon</code> keys.
</p>
</div>
<div className="flex flex-col items-end">
{isUpdatingJwtSecret ? (
@@ -244,13 +249,13 @@ const JWTSettings = () => {
},
}}
>
Generate a new secret
Generate new API Keys
</ButtonTooltip>
) : (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="default" iconRight={<ChevronDown size={14} />}>
<span>Generate a new secret</span>
<span>Generate new API Keys</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="bottom">
@@ -259,7 +264,7 @@ const JWTSettings = () => {
onClick={() => setIsGeneratingKey(true)}
>
<RefreshCw size={16} />
<p>Generate a random secret</p>
<p>Generate new API Keys</p>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
@@ -4,11 +4,13 @@ import { useParams } from 'common'
import Panel from 'components/ui/Panel'
import { useJwtSecretUpdatingStatusQuery } from 'data/config/jwt-secret-updating-status-query'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useServiceRoleKeyLeakQuery } from 'data/lint/service-role-key-leak-query'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useFlag } from 'hooks/ui/useFlag'
import { AlertCircle, Loader2 } from 'lucide-react'
import { Input } from 'ui'
import Link from 'next/link'
import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, Input, WarningIcon } from 'ui'
const DisplayApiSettings = ({
legacy,
showNotice = true,
@@ -41,7 +43,9 @@ const DisplayApiSettings = ({
const apiKeys = settings?.service_api_keys ?? []
// api keys should not be empty. However it can be populated with a delay on project creation
const isApiKeysEmpty = apiKeys.length === 0
const { data: hasServiceRoleKeyLeak } = useServiceRoleKeyLeakQuery({
projectRef: 'bzembqluzmwrjuidwley', // remove before merging
})
return (
<>
<Panel
@@ -143,6 +147,32 @@ const DisplayApiSettings = ({
(legacy ? 'Prefer using Secret API keys instead.' : '')
}
/>
{hasServiceRoleKeyLeak && x.tags === 'service_role' && (
<Alert_Shadcn_ variant="destructive" className="my-4">
<WarningIcon />
<AlertTitle_Shadcn_>Urgent: Service Role key leak detected</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_ className="max-w-2xl">
<p>
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.{' '}
<span className="font-bold">Generate new API keys</span> below in JWT Settings
to rotate this key.
</p>
<p></p>
<p className="mt-2">
Read{' '}
<Link
className="underline"
href="https://supabase.com/docs/guides/api/api-keys"
>
Understanding API Keys
</Link>{' '}
to learn more.
</p>
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
)}
</Panel.Content>
))
)}
@@ -161,7 +191,7 @@ const DisplayApiSettings = ({
) : (
<Panel.Notice
className="border-t"
title="New API keys coming Q4 2024"
title="New API keys coming in 2025"
description={`
\`anon\` and \`service_role\` API keys will be changing to \`publishable\` and \`secret\` API keys.
`}
+2
View File
@@ -1,3 +1,5 @@
export const lintKeys = {
lint: (projectRef: string | undefined) => ['projects', projectRef, 'lint'] as const,
serviceRoleKeyLeak: (projectRef: string | undefined) =>
['projects', projectRef, 'service-role-key-leak'] as const,
}
@@ -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<ServiceRoleKeyLeakResponse> {
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<ReturnType<typeof checkServiceRoleKeyLeak>>
export type ServiceRoleKeyLeakError = ResponseError
export const useServiceRoleKeyLeakQuery = <TData = ServiceRoleKeyLeakData>(
{ projectRef }: ServiceRoleKeyLeakVariables,
{
enabled = true,
...options
}: UseQueryOptions<ServiceRoleKeyLeakData, ServiceRoleKeyLeakError, TData> = {}
) => {
const project = useSelectedProject()
const isActive = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY
return useQuery<ServiceRoleKeyLeakData, ServiceRoleKeyLeakError, TData>(
lintKeys.serviceRoleKeyLeak(projectRef),
({ signal }) => checkServiceRoleKeyLeak({ projectRef }, signal),
{
enabled: enabled && typeof projectRef !== 'undefined' && isActive,
...options,
}
)
}
+1
View File
@@ -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) {
@@ -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