mirror of
https://github.com/supabase/supabase.git
synced 2026-10-17 15:19:08 +03:00
Check for leaked keys
This commit is contained in:
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.
|
||||
`}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
Reference in new issue
Block a user