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 지원하기

기본 정보

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):

bash
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/DELETE on 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 OPTION and complementary RLS on base tables to ensure only allowed rows can be written/changed.
  • In Supabase, avoid granting anon/authenticated any write privileges on views unless you have verified end-to-end behavior with tests.

Detection tip:

  • From anon and an authenticated test 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):

bash
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 예상):
bash
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를 사용하세요):
bash
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 테스트가 차단됨:
bash
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 테스트가 차단됨:
bash
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 anon and a minimally authenticated user 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 a secret access key (e.g. 58420818223133077c2cec6712a4f909aec93b4daeedae205aa8e30d5a860628)

Edge Functions

supabase에도 secrets를 저장할 수 있으며 이는 edge functions에서 접근 가능합니다(웹에서 생성 및 삭제할 수 있지만, 값 자체에는 직접 접근할 수 없습니다).

References

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 지원하기