Supabase 보안
Reading time: 10 minutes
tip
AWS 해킹 배우기 및 연습하기:
HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기:
HackTricks Training GCP Red Team Expert (GRTE)
Azure 해킹 배우기 및 연습하기:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricks 지원하기
- 구독 계획 확인하기!
- **💬 Discord 그룹 또는 텔레그램 그룹에 참여하거나 Twitter 🐦 @hacktricks_live를 팔로우하세요.
- HackTricks 및 HackTricks Cloud 깃허브 리포지토리에 PR을 제출하여 해킹 트릭을 공유하세요.
기본 정보
As per their landing page: Supabase is an open source Firebase alternative. Start your project with a Postgres database, Authentication, instant APIs, Edge Functions, Realtime subscriptions, Storage, and Vector embeddings.
서브도메인
기본적으로 프로젝트가 생성되면 사용자에게 다음과 같은 supabase.co 서브도메인이 부여됩니다: jnanozjdybtpqgcwhdiz.supabase.co
데이터베이스 구성
tip
이 데이터는 https://supabase.com/dashboard/project/<project-id>/settings/database 같은 링크에서 접근할 수 있습니다
이 데이터베이스는 특정 AWS 리전에 배포되며, 연결하려면 다음과 같은 주소로 연결하면 됩니다: postgres://postgres.jnanozjdybtpqgcwhdiz:[YOUR-PASSWORD]@aws-0-us-west-1.pooler.supabase.com:5432/postgres (이 예시는 us-west-1에 생성되었습니다).
비밀번호는 사용자가 이전에 설정한 비밀번호입니다.
따라서 서브도메인이 알려져 있고 사용자 이름으로 사용되며 AWS 리전이 제한적이기 때문에 brute force the password를 시도해 볼 수 있습니다.
이 섹션에는 다음 옵션들도 포함됩니다:
- 데이터베이스 비밀번호 재설정
- 연결 풀(connection pooling) 구성
- SSL 구성: 평문 연결 거부(기본값으로 평문 연결이 허용되어 있음)
- 디스크 크기 구성
- 네트워크 제한 및 차단 적용
API 구성
tip
이 데이터는 https://supabase.com/dashboard/project/<project-id>/settings/api 같은 링크에서 접근할 수 있습니다
프로젝트의 supabase API에 접근하는 URL은 다음과 같습니다: https://jnanozjdybtpqgcwhdiz.supabase.co.
anon API 키
또한 anon API key (role: "anon"), 예: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpuYW5vemRyb2J0cHFnY3doZGl6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTQ5OTI3MTksImV4cCI6MjAzMDU2ODcxOX0.sRN0iMGM5J741pXav7UxeChyqBE9_Z-T0tLA9Zehvqk 이 생성되며, 애플리케이션이 API에 접근하기 위해 사용해야 합니다.
이 API와 통신할 REST API는 docs에서 확인할 수 있지만, 가장 흥미로운 엔드포인트는 다음과 같습니다:
회원가입 (/auth/v1/signup)
``` POST /auth/v1/signup HTTP/2 Host: id.io.net Content-Length: 90 X-Client-Info: supabase-js-web/2.39.2 Sec-Ch-Ua: "Not-A.Brand";v="99", "Chromium";v="124" Sec-Ch-Ua-Mobile: ?0 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpuYW5vemRyb2J0cHFnY3doZGl6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTQ5OTI3MTksImV4cCI6MjAzMDU2ODcxOX0.sRN0iMGM5J741pXav7UxeChyqBE9_Z-T0tLA9Zehvqk User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.60 Safari/537.36 Content-Type: application/json;charset=UTF-8 Apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpuYW5vemRyb2J0cHFnY3doZGl6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTQ5OTI3MTksImV4cCI6MjAzMDU2ODcxOX0.sRN0iMGM5J741pXav7UxeChyqBE9_Z-T0tLA9Zehvqk Sec-Ch-Ua-Platform: "macOS" Accept: */* Origin: https://cloud.io.net Sec-Fetch-Site: same-site Sec-Fetch-Mode: cors Sec-Fetch-Dest: empty Referer: https://cloud.io.net/ Accept-Encoding: gzip, deflate, br Accept-Language: en-GB,en-US;q=0.9,en;q=0.8 Priority: u=1, i{"email":"test@exmaple.com","password":"SomeCOmplexPwd239."}
</details>
<details>
<summary>로그인 (/auth/v1/token?grant_type=password)</summary>
POST /auth/v1/token?grant_type=password HTTP/2 Host: hypzbtgspjkludjcnjxl.supabase.co Content-Length: 80 X-Client-Info: supabase-js-web/2.39.2 Sec-Ch-Ua: "Not-A.Brand";v="99", "Chromium";v="124" Sec-Ch-Ua-Mobile: ?0 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpuYW5vemRyb2J0cHFnY3doZGl6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTQ5OTI3MTksImV4cCI6MjAzMDU2ODcxOX0.sRN0iMGM5J741pXav7UxeChyqBE9_Z-T0tLA9Zehvqk User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.6367.60 Safari/537.36 Content-Type: application/json;charset=UTF-8 Apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpuYW5vemRyb2J0cHFnY3doZGl6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTQ5OTI3MTksImV4cCI6MjAzMDU2ODcxOX0.sRN0iMGM5J741pXav7UxeChyqBE9_Z-T0tLA9Zehvqk Sec-Ch-Ua-Platform: "macOS" Accept: / Origin: https://cloud.io.net Sec-Fetch-Site: same-site Sec-Fetch-Mode: cors Sec-Fetch-Dest: empty Referer: https://cloud.io.net/ Accept-Encoding: gzip, deflate, br Accept-Language: en-GB,en-US;q=0.9,en;q=0.8 Priority: u=1, i
{"email":"test@exmaple.com","password":"SomeCOmplexPwd239."}
</details>
따라서 클라이언트가 부여받은 서브도메인으로 supabase를 사용하고 있는 것을 발견하면(회사 도메인의 서브도메인이 supabase 서브도메인에 CNAME을 설정했을 가능성이 있음), **supabase API를 사용해 플랫폼에 새 계정을 생성해 볼 수 있습니다**.
### secret / service_role api keys
A secret API key will also be generated with **`role: "service_role"`**. This API key should be secret because it will be able to bypass **Row Level Security**.
The API key looks like this: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpuYW5vemRyb2J0cHFnY3doZGl6Iiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTcxNDk5MjcxOSwiZXhwIjoyMDMwNTY4NzE5fQ.0a8fHGp3N_GiPq0y0dwfs06ywd-zhTwsm486Tha7354`
### JWT Secret
A **JWT Secret** will also be generate so the application can **create and sign custom JWT tokens**.
## 인증
### 회원가입
<div class="mdbook-alerts mdbook-alerts-tip">
<p class="mdbook-alerts-title">
<span class="mdbook-alerts-icon"></span>
tip
</p>
기본적으로 supabase는 앞서 언급한 API 엔드포인트를 사용해 **새 사용자가 프로젝트에 계정을 생성하는 것**을 허용합니다.
</div>
그러나 이러한 새 계정은 기본적으로 **로그인하려면 이메일 주소를 검증해야 합니다**. 이메일 검증 없이 로그인할 수 있도록 **"Allow anonymous sign-ins"**를 활성화할 수 있습니다. 이 경우 사용자들은 이메일 확인 없이 로그인할 수 있으며, **예상치 못한 데이터**에 접근할 수 있게 될 수 있습니다(이들은 `public` 및 `authenticated` 역할을 부여받습니다).\
이것은 매우 나쁜 생각입니다. supabase는 활성 사용자 수에 따라 요금을 청구하므로 사람들이 계정을 생성하고 로그인하면 supabase에 비용이 발생합니다:
<figure><img src="../images/image (1) (1) (1) (1) (1) (1).png" alt=""><figcaption></figcaption></figure>
#### Auth: 서버 측 회원가입 제어
프론트엔드에서 가입 버튼을 숨기는 것만으로는 충분하지 않습니다. **Auth 서버가 여전히 가입을 허용하면**, 공격자는 퍼블릭 `anon` 키로 API를 직접 호출해 임의의 사용자를 생성할 수 있습니다.
빠른 테스트 (비인증 클라이언트에서):
<div class="codeblock_filename_container"><span class="codeblock_filename_inner hljs">bash</span></div>
```bash
curl -X POST \
-H "apikey: <SUPABASE_ANON_KEY>" \
-H "Authorization: Bearer <SUPABASE_ANON_KEY>" \
-H "Content-Type: application/json" \
-d '{"email":"attacker@example.com","password":"Sup3rStr0ng!"}' \
https://<PROJECT_REF>.supabase.co/auth/v1/signup
Expected hardening:
- Disable email/password signups in the Dashboard: Authentication → Providers → Email → Disable sign ups (invite-only), or set the equivalent GoTrue setting.
- API가 이전 호출에 대해 이제 4xx를 반환하고 새로운 사용자가 생성되지 않는지 확인하세요.
- invites 또는 SSO에 의존하는 경우, 명시적으로 필요하지 않은 모든 다른 providers는 비활성화되어 있는지 확인하세요.
RLS 및 Views: PostgREST를 통한 쓰기 우회
Postgres VIEW를 사용해 민감한 컬럼을 “숨기고” 이를 PostgREST로 노출하면 권한 평가 방식이 달라질 수 있습니다. PostgreSQL에서는:
- 일반적인 views는 기본적으로 view owner의 권한으로 실행됩니다 (definer semantics). PG ≥15에서는
security_invoker를 선택할 수 있습니다. - Row Level Security (RLS)는 base tables에 적용됩니다. Table owners는 테이블에
FORCE ROW LEVEL SECURITY가 설정되지 않은 한 RLS를 우회합니다. - Updatable views는 INSERT/UPDATE/DELETE를 받아 base table에 적용될 수 있습니다.
WITH CHECK OPTION이 없으면 view 조건에 맞지 않는 쓰기 요청도 성공할 수 있습니다.
현장에서 관찰된 위험 패턴:
- 컬럼을 줄인 view가 Supabase REST를 통해 노출되고
anon/authenticated에 권한이 부여된다. - PostgREST는 updatable view에 대한 DML을 허용하고, 해당 연산은 view owner의 권한으로 평가되어 base table에 대한 의도된 RLS 정책을 사실상 우회한다.
- 결과: 권한이 낮은 클라이언트가 수정해서는 안 되는 행들(예: profile bios/avatars)을 대량으로 편집할 수 있다.
Illustrative write via view (attempted from a public client):
curl -X PATCH \
-H "apikey: <SUPABASE_ANON_KEY>" \
-H "Authorization: Bearer <SUPABASE_ANON_KEY>" \
-H "Content-Type: application/json" \
-H "Prefer: return=representation" \
-d '{"bio":"pwned","avatar_url":"https://i.example/pwn.png"}' \
"https://<PROJECT_REF>.supabase.co/rest/v1/users_view?id=eq.<victim_user_id>"
Hardening checklist for views and RLS:
- Prefer exposing base tables with explicit, least-privilege grants and precise RLS policies.
- If you must expose a view:
- Make it non-updatable (e.g., include expressions/joins) or deny
INSERT/UPDATE/DELETEon the view to all untrusted roles. - Enforce
ALTER VIEW <v> SET (security_invoker = on)so the invoker’s privileges are used instead of the owner’s. - On base tables, use
ALTER TABLE <t> FORCE ROW LEVEL SECURITY;so even owners are subject to RLS. - If allowing writes via an updatable view, add
WITH [LOCAL|CASCADED] CHECK OPTIONand complementary RLS on base tables to ensure only allowed rows can be written/changed. - In Supabase, avoid granting
anon/authenticatedany write privileges on views unless you have verified end-to-end behavior with tests.
Detection tip:
- From
anonand anauthenticatedtest user, attempt all CRUD operations against every exposed table/view. Any successful write where you expected denial indicates a misconfiguration.
OpenAPI-driven CRUD probing from anon/auth roles
PostgREST exposes an OpenAPI document that you can use to enumerate all REST resources, then automatically probe allowed operations from low-privileged roles.
Fetch the OpenAPI (works with the public anon key):
curl -s https://<PROJECT_REF>.supabase.co/rest/v1/ \
-H "apikey: <SUPABASE_ANON_KEY>" \
-H "Authorization: Bearer <SUPABASE_ANON_KEY>" \
-H "Accept: application/openapi+json" | jq '.paths | keys[]'
프로브 패턴 (예시):
- 단일 행 읽기 (RLS에 따라 401/403/200 예상):
curl -s "https://<PROJECT_REF>.supabase.co/rest/v1/<table>?select=*&limit=1" \
-H "apikey: <SUPABASE_ANON_KEY>" \
-H "Authorization: Bearer <SUPABASE_ANON_KEY>"
- UPDATE가 차단되었는지 테스트 (테스트 중 데이터 변경을 피하려면 존재하지 않는 filter를 사용하세요):
curl -i -X PATCH \
-H "apikey: <SUPABASE_ANON_KEY>" \
-H "Authorization: Bearer <SUPABASE_ANON_KEY>" \
-H "Content-Type: application/json" \
-H "Prefer: return=minimal" \
-d '{"__probe":true}' \
"https://<PROJECT_REF>.supabase.co/rest/v1/<table_or_view>?id=eq.00000000-0000-0000-0000-000000000000"
- INSERT 테스트가 차단됨:
curl -i -X POST \
-H "apikey: <SUPABASE_ANON_KEY>" \
-H "Authorization: Bearer <SUPABASE_ANON_KEY>" \
-H "Content-Type: application/json" \
-H "Prefer: return=minimal" \
-d '{"__probe":true}' \
"https://<PROJECT_REF>.supabase.co/rest/v1/<table_or_view>"
- DELETE 테스트가 차단됨:
curl -i -X DELETE \
-H "apikey: <SUPABASE_ANON_KEY>" \
-H "Authorization: Bearer <SUPABASE_ANON_KEY>" \
"https://<PROJECT_REF>.supabase.co/rest/v1/<table_or_view>?id=eq.00000000-0000-0000-0000-000000000000"
Recommendations:
- Automate the previous probes for both
anonand a minimallyauthenticateduser and integrate them in CI to catch regressions. - Treat every exposed table/view/function as a first-class surface. Don’t assume a view “inherits” the same RLS posture as its base tables.
Passwords & sessions
최소 비밀번호 길이(기본값), 요구사항(기본적으로 없음)을 지정하고 leaked passwords 사용을 금지할 수 있습니다.
기본 요구사항이 약하므로 요구사항을 강화하는 것이 권장됩니다.
- User Sessions: 사용자 세션 동작(타임아웃, 1 사용자당 1 세션 등)을 구성할 수 있습니다.
- Bot and Abuse Protection: Captcha를 활성화할 수 있습니다.
SMTP Settings
이메일 전송을 위해 SMTP를 설정할 수 있습니다.
Advanced Settings
- Set expire time to access tokens (3600 by default)
- 잠재적으로 compromised된 refresh tokens를 탐지하고 폐기하며 타임아웃을 설정합니다
- MFA: 사용자당 동시에 등록할 수 있는 MFA 요소 수를 지정합니다(기본값 10)
- Max Direct Database Connections: 인증에 사용되는 최대 연결 수(기본값 10)
- Max Request Duration: Auth 요청이 허용되는 최대 지속 시간(기본값 10s)
Storage
tip
Supabase는 파일을 저장하고 URL을 통해 접근 가능하게 만들 수 있습니다 (S3 buckets를 사용합니다).
- 업로드 파일 크기 제한을 설정합니다(기본값 50MB)
- The S3 connection is given with a URL like:
https://jnanozjdybtpqgcwhdiz.supabase.co/storage/v1/s3 - It's possible to request S3 access key that are formed by an
access key ID(e.g.a37d96544d82ba90057e0e06131d0a7b) and asecret access key(e.g.58420818223133077c2cec6712a4f909aec93b4daeedae205aa8e30d5a860628)
Edge Functions
supabase에도 secrets를 저장할 수 있으며 이는 edge functions에서 접근 가능합니다(웹에서 생성 및 삭제할 수 있지만, 값 자체에는 직접 접근할 수 없습니다).
References
- Building Hacker Communities: Bug Bounty Village, getDisclosed’s Supabase Misconfig, and the LHE Squad (Ep. 133) – YouTube
- Critical Thinking Podcast – Episode 133 page
- Supabase: Row Level Security (RLS)
- PostgreSQL: Row Security Policies
- PostgreSQL: CREATE VIEW (security_invoker, check option)
- PostgREST: OpenAPI documentation
tip
AWS 해킹 배우기 및 연습하기:
HackTricks Training AWS Red Team Expert (ARTE)
GCP 해킹 배우기 및 연습하기:
HackTricks Training GCP Red Team Expert (GRTE)
Azure 해킹 배우기 및 연습하기:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricks 지원하기
- 구독 계획 확인하기!
- **💬 Discord 그룹 또는 텔레그램 그룹에 참여하거나 Twitter 🐦 @hacktricks_live를 팔로우하세요.
- HackTricks 및 HackTricks Cloud 깃허브 리포지토리에 PR을 제출하여 해킹 트릭을 공유하세요.
HackTricks Cloud