openhack 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- openhack/__init__.py +2 -0
- openhack/__main__.py +225 -0
- openhack/agents/__init__.py +30 -0
- openhack/agents/base.py +230 -0
- openhack/agents/browser_verifier.py +679 -0
- openhack/agents/browser_verifier_swarm.py +256 -0
- openhack/agents/checkpoint.py +89 -0
- openhack/agents/context_manager.py +356 -0
- openhack/agents/coordinator.py +1105 -0
- openhack/agents/endpoint_analyst.py +307 -0
- openhack/agents/feature_hunter.py +93 -0
- openhack/agents/hunter.py +481 -0
- openhack/agents/hunter_swarm.py +385 -0
- openhack/agents/llm.py +334 -0
- openhack/agents/recon.py +19 -0
- openhack/agents/sandbox_verifier.py +396 -0
- openhack/agents/sandbox_verifier_swarm.py +250 -0
- openhack/agents/session.py +286 -0
- openhack/agents/validator.py +217 -0
- openhack/agents/validator_swarm.py +106 -0
- openhack/auth.py +175 -0
- openhack/browser/__init__.py +12 -0
- openhack/browser/runner.py +385 -0
- openhack/categories.py +130 -0
- openhack/config.py +201 -0
- openhack/deterministic_recon.py +464 -0
- openhack/entry_points.py +745 -0
- openhack/framework_classifier.py +515 -0
- openhack/framework_detection.py +269 -0
- openhack/headless_scan.py +179 -0
- openhack/prompts/__init__.py +108 -0
- openhack/prompts/browser_verifier.py +171 -0
- openhack/prompts/coordinator.py +31 -0
- openhack/prompts/django/__init__.py +32 -0
- openhack/prompts/django/auth_bypass.py +76 -0
- openhack/prompts/django/csrf.py +62 -0
- openhack/prompts/django/data_exposure.py +67 -0
- openhack/prompts/django/idor.py +74 -0
- openhack/prompts/django/injection.py +67 -0
- openhack/prompts/django/misconfiguration.py +70 -0
- openhack/prompts/django/ssrf.py +64 -0
- openhack/prompts/endpoint_analyst.py +122 -0
- openhack/prompts/express/__init__.py +29 -0
- openhack/prompts/express/auth_bypass.py +71 -0
- openhack/prompts/express/data_exposure.py +77 -0
- openhack/prompts/express/idor.py +69 -0
- openhack/prompts/express/injection.py +75 -0
- openhack/prompts/express/misconfiguration.py +72 -0
- openhack/prompts/express/ssrf.py +63 -0
- openhack/prompts/feature_hunter.py +140 -0
- openhack/prompts/flask/__init__.py +29 -0
- openhack/prompts/flask/auth_bypass.py +86 -0
- openhack/prompts/flask/data_exposure.py +78 -0
- openhack/prompts/flask/idor.py +83 -0
- openhack/prompts/flask/injection.py +77 -0
- openhack/prompts/flask/misconfiguration.py +73 -0
- openhack/prompts/flask/ssrf.py +65 -0
- openhack/prompts/hunter.py +362 -0
- openhack/prompts/hunter_continuation_loop.py +12 -0
- openhack/prompts/hunter_continuation_no_findings.py +19 -0
- openhack/prompts/hunter_continuation_no_progress.py +22 -0
- openhack/prompts/hunter_tool_instructions.py +55 -0
- openhack/prompts/nextjs/__init__.py +42 -0
- openhack/prompts/nextjs/auth_bypass.py +80 -0
- openhack/prompts/nextjs/csrf.py +71 -0
- openhack/prompts/nextjs/data_exposure.py +88 -0
- openhack/prompts/nextjs/idor.py +64 -0
- openhack/prompts/nextjs/injection.py +65 -0
- openhack/prompts/nextjs/middleware_bypass.py +75 -0
- openhack/prompts/nextjs/misconfiguration.py +92 -0
- openhack/prompts/nextjs/server_actions.py +97 -0
- openhack/prompts/nextjs/ssrf.py +66 -0
- openhack/prompts/nextjs/xss.py +69 -0
- openhack/prompts/pr_analysis_system.py +80 -0
- openhack/prompts/pr_analysis_user.py +11 -0
- openhack/prompts/project_context.py +89 -0
- openhack/prompts/recon.py +199 -0
- openhack/prompts/reporter.py +88 -0
- openhack/prompts/researchers.py +434 -0
- openhack/prompts/sandbox_verifier.py +128 -0
- openhack/prompts/supabase/__init__.py +39 -0
- openhack/prompts/supabase/auth_tokens.py +131 -0
- openhack/prompts/supabase/edge_functions.py +150 -0
- openhack/prompts/supabase/graphql.py +102 -0
- openhack/prompts/supabase/postgrest.py +99 -0
- openhack/prompts/supabase/realtime.py +93 -0
- openhack/prompts/supabase/rls.py +110 -0
- openhack/prompts/supabase/rpc_functions.py +127 -0
- openhack/prompts/supabase/storage.py +110 -0
- openhack/prompts/supabase/tenant_isolation.py +118 -0
- openhack/prompts/validator.py +319 -0
- openhack/prompts/validator_continuation_incomplete.py +12 -0
- openhack/prompts/validator_tool_instructions.py +29 -0
- openhack/quality.py +231 -0
- openhack/sandbox/__init__.py +12 -0
- openhack/sandbox/orchestrator.py +517 -0
- openhack/sandbox/runner.py +177 -0
- openhack/scan_session.py +245 -0
- openhack/setup.py +452 -0
- openhack/static_validator.py +612 -0
- openhack/tools/__init__.py +1 -0
- openhack/tools/ast_tools.py +307 -0
- openhack/tools/coverage.py +1078 -0
- openhack/tools/filesystem.py +404 -0
- openhack/tools/nextjs.py +258 -0
- openhack/tools/registry.py +52 -0
- openhack/tui.py +3450 -0
- openhack/updates.py +170 -0
- openhack-0.1.0.dist-info/METADATA +189 -0
- openhack-0.1.0.dist-info/RECORD +113 -0
- openhack-0.1.0.dist-info/WHEEL +4 -0
- openhack-0.1.0.dist-info/entry_points.txt +2 -0
- openhack-0.1.0.dist-info/licenses/LICENSE +661 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Supabase Auth, JWT, and service_role key exposure detection prompt.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
SUPABASE_AUTH_PROMPT = """## Auth & JWT Security in Supabase
|
|
6
|
+
|
|
7
|
+
### Pre-Computed Recon Available
|
|
8
|
+
|
|
9
|
+
The `supabase_recon` context already contains:
|
|
10
|
+
- `supabase_recon.clients` -- all Supabase client initializations with `uses_service_role` flag
|
|
11
|
+
- `supabase_recon.config.env_vars` -- Supabase-related environment variable names found in the codebase
|
|
12
|
+
- `supabase_recon.edge_functions` -- Edge Functions with `uses_service_role` flag
|
|
13
|
+
|
|
14
|
+
**Immediate critical findings:**
|
|
15
|
+
- Any entry in `clients` with `uses_service_role: True` in a client-side file (e.g., under `app/`, `pages/`, `components/`, or any file imported by client code)
|
|
16
|
+
- `NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY` in `config.env_vars` (service role key exposed to browser via Next.js public env var)
|
|
17
|
+
|
|
18
|
+
### What to Look For
|
|
19
|
+
|
|
20
|
+
1. **service_role key exposed in client code**
|
|
21
|
+
- The `service_role` key bypasses ALL RLS policies -- it's the master key
|
|
22
|
+
- Must NEVER appear in client-side code, browser bundles, or public env vars
|
|
23
|
+
- Check: `NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY`, `VITE_SUPABASE_SERVICE_ROLE_KEY`
|
|
24
|
+
- Check: `createClient(url, serviceRoleKey)` in files served to the browser
|
|
25
|
+
|
|
26
|
+
2. **Hardcoded keys in source code**
|
|
27
|
+
- API keys, service role keys, or JWT secrets committed to the repository
|
|
28
|
+
- `grep` for long base64 strings near Supabase client initialization
|
|
29
|
+
- Check `.env.local`, `.env.example`, `.env` files for real key values
|
|
30
|
+
|
|
31
|
+
3. **getSession() vs getUser() confusion**
|
|
32
|
+
- `getSession()` reads the session from local storage -- it is NOT authenticated by the server
|
|
33
|
+
- An attacker can forge the session in localStorage and `getSession()` will trust it
|
|
34
|
+
- `getUser()` makes a server call to verify the JWT -- this is the safe method
|
|
35
|
+
- Server-side code should always use `getUser()` for authorization decisions
|
|
36
|
+
|
|
37
|
+
4. **JWT stored in localStorage**
|
|
38
|
+
- Tokens in localStorage are accessible to any JavaScript on the page (XSS-exfiltrable)
|
|
39
|
+
- Supabase stores tokens in localStorage by default
|
|
40
|
+
- If the app has any XSS vulnerability, auth tokens can be stolen
|
|
41
|
+
|
|
42
|
+
5. **Missing audience/issuer verification**
|
|
43
|
+
- Custom endpoints that accept JWTs should verify `iss` (issuer) and `aud` (audience)
|
|
44
|
+
- Tokens from one Supabase project should not work on another
|
|
45
|
+
- Edge Functions should not trust the `Authorization` header without verification
|
|
46
|
+
|
|
47
|
+
6. **apikey treated as identity**
|
|
48
|
+
- The `apikey` header identifies the project, NOT the user
|
|
49
|
+
- The anon key is public and project-scoped
|
|
50
|
+
- Code that checks for `apikey` presence as an auth mechanism is vulnerable
|
|
51
|
+
|
|
52
|
+
7. **Refresh token mismanagement**
|
|
53
|
+
- Long-lived refresh tokens that never expire
|
|
54
|
+
- Refresh tokens not rotated after use
|
|
55
|
+
- Revocation not implemented for compromised tokens
|
|
56
|
+
|
|
57
|
+
### Vulnerable Patterns
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
// VULNERABLE: service_role key in client-side code
|
|
61
|
+
import { createClient } from '@supabase/supabase-js'
|
|
62
|
+
const supabase = createClient(
|
|
63
|
+
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
|
64
|
+
process.env.NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY! // EXPOSED TO BROWSER!
|
|
65
|
+
)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
// VULNERABLE: Using getSession() for authorization
|
|
70
|
+
export async function GET() {
|
|
71
|
+
const { data: { session } } = await supabase.auth.getSession()
|
|
72
|
+
if (!session) return new Response('Unauthorized', { status: 401 })
|
|
73
|
+
// session could be forged from localStorage!
|
|
74
|
+
const userId = session.user.id // CANNOT BE TRUSTED
|
|
75
|
+
const data = await supabase.from('orders').select().eq('user_id', userId)
|
|
76
|
+
return Response.json(data)
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// VULNERABLE: Treating apikey as authentication
|
|
82
|
+
export async function middleware(req) {
|
|
83
|
+
const apiKey = req.headers.get('apikey')
|
|
84
|
+
if (!apiKey) return new Response('Unauthorized', { status: 401 })
|
|
85
|
+
// apikey is public! This is NOT authentication
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Safe Patterns
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
// SAFE: service_role key only in server-side code
|
|
93
|
+
// lib/supabase/admin.ts (never imported by client code)
|
|
94
|
+
import { createClient } from '@supabase/supabase-js'
|
|
95
|
+
export const supabaseAdmin = createClient(
|
|
96
|
+
process.env.SUPABASE_URL!, // No NEXT_PUBLIC_ prefix
|
|
97
|
+
process.env.SUPABASE_SERVICE_ROLE_KEY! // No NEXT_PUBLIC_ prefix
|
|
98
|
+
)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
// SAFE: Using getUser() for authorization
|
|
103
|
+
export async function GET() {
|
|
104
|
+
const { data: { user }, error } = await supabase.auth.getUser()
|
|
105
|
+
if (error || !user) return new Response('Unauthorized', { status: 401 })
|
|
106
|
+
// user.id is verified by the server
|
|
107
|
+
const data = await supabase.from('orders').select().eq('user_id', user.id)
|
|
108
|
+
return Response.json(data)
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Search Patterns
|
|
113
|
+
|
|
114
|
+
1. Check `supabase_recon.clients` for `uses_service_role: True` -- inspect the file path to determine if client-side
|
|
115
|
+
2. Check `supabase_recon.config.env_vars` for `NEXT_PUBLIC_*SERVICE_ROLE*` or `VITE_*SERVICE_ROLE*`
|
|
116
|
+
3. `grep` for `service_role`, `serviceRole`, `SUPABASE_SERVICE_ROLE` in all files
|
|
117
|
+
4. `grep` for `getSession` in server-side files (API routes, server components, middleware) -- should be `getUser`
|
|
118
|
+
5. `grep` for `localStorage` near token storage/retrieval
|
|
119
|
+
6. `grep` for hardcoded JWT-like strings (long base64 with dots: `eyJ...`)
|
|
120
|
+
7. Check `.env*` files for real key values committed to repo
|
|
121
|
+
|
|
122
|
+
### Severity Assessment
|
|
123
|
+
|
|
124
|
+
- **Critical**: service_role key exposed in client bundle or public env var
|
|
125
|
+
- **Critical**: Hardcoded service_role key in committed source code
|
|
126
|
+
- **High**: Using `getSession()` instead of `getUser()` for server-side authorization
|
|
127
|
+
- **High**: Custom endpoints accepting JWTs without audience/issuer verification
|
|
128
|
+
- **Medium**: JWT tokens stored in localStorage (XSS risk)
|
|
129
|
+
- **Medium**: apikey header used as authentication mechanism
|
|
130
|
+
- **Low**: Refresh token configuration concerns
|
|
131
|
+
"""
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Supabase Edge Functions security detection prompt.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
SUPABASE_EDGE_FUNCTIONS_PROMPT = """## Edge Functions Security in Supabase
|
|
6
|
+
|
|
7
|
+
### Pre-Computed Recon Available
|
|
8
|
+
|
|
9
|
+
The `supabase_recon` context already contains:
|
|
10
|
+
- `supabase_recon.edge_functions` -- all discovered Edge Functions with `uses_service_role`, `has_cors`, `has_auth_check`, `has_jwt_verification` flags
|
|
11
|
+
|
|
12
|
+
**Immediate findings: any Edge Function with `uses_service_role: True` + `has_auth_check: False` is high risk -- it has full database access but doesn't verify who's calling it.**
|
|
13
|
+
|
|
14
|
+
### What to Look For
|
|
15
|
+
|
|
16
|
+
1. **service_role usage without JWT verification**
|
|
17
|
+
- Edge Functions commonly initialize Supabase with `SUPABASE_SERVICE_ROLE_KEY` for admin operations
|
|
18
|
+
- If the function doesn't verify the caller's JWT, anyone can trigger admin-level database operations
|
|
19
|
+
- The function must extract and verify the JWT from the `Authorization` header
|
|
20
|
+
|
|
21
|
+
2. **CORS misconfiguration**
|
|
22
|
+
- Wildcard `Access-Control-Allow-Origin: *` combined with `Access-Control-Allow-Credentials: true`
|
|
23
|
+
- Reflected `Origin` header in `Access-Control-Allow-Origin` without validation
|
|
24
|
+
- Missing CORS headers allowing any origin to call the function
|
|
25
|
+
|
|
26
|
+
3. **SSRF via fetch()**
|
|
27
|
+
- Edge Functions can make outbound HTTP requests
|
|
28
|
+
- If the URL is controlled by user input, the function can be used to probe internal services
|
|
29
|
+
- Metadata service access: `http://169.254.169.254/` or cloud provider metadata endpoints
|
|
30
|
+
|
|
31
|
+
4. **Secrets in error traces or logs**
|
|
32
|
+
- Error responses that include stack traces with environment variable values
|
|
33
|
+
- `console.log` or `console.error` dumping request/response bodies containing secrets
|
|
34
|
+
- Service role key or other secrets appearing in response bodies on error
|
|
35
|
+
|
|
36
|
+
5. **Not re-deriving user from JWT**
|
|
37
|
+
- Trusting `user_id` or `tenant_id` from the request body instead of extracting from JWT
|
|
38
|
+
- The function should parse the JWT to get the authenticated user identity
|
|
39
|
+
|
|
40
|
+
6. **Missing auth on function invocation**
|
|
41
|
+
- Edge Functions accessible without any Authorization header
|
|
42
|
+
- No validation that the caller is authenticated at all
|
|
43
|
+
|
|
44
|
+
### Vulnerable Patterns
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// supabase/functions/process-order/index.ts
|
|
48
|
+
// VULNERABLE: service_role with no auth check
|
|
49
|
+
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
|
|
50
|
+
|
|
51
|
+
Deno.serve(async (req) => {
|
|
52
|
+
const { orderId, userId } = await req.json()
|
|
53
|
+
|
|
54
|
+
// Uses service_role (bypasses all RLS)
|
|
55
|
+
const supabase = createClient(
|
|
56
|
+
Deno.env.get('SUPABASE_URL')!,
|
|
57
|
+
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
// Trusts client-supplied userId instead of extracting from JWT
|
|
61
|
+
const { data } = await supabase
|
|
62
|
+
.from('orders')
|
|
63
|
+
.update({ status: 'processed' })
|
|
64
|
+
.eq('id', orderId)
|
|
65
|
+
.eq('user_id', userId) // Client controls this!
|
|
66
|
+
|
|
67
|
+
return new Response(JSON.stringify(data))
|
|
68
|
+
})
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// VULNERABLE: Wildcard CORS with credentials
|
|
73
|
+
const corsHeaders = {
|
|
74
|
+
'Access-Control-Allow-Origin': '*',
|
|
75
|
+
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
|
|
76
|
+
'Access-Control-Allow-Credentials': 'true', // Dangerous with wildcard origin!
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// VULNERABLE: SSRF via user-controlled URL
|
|
82
|
+
Deno.serve(async (req) => {
|
|
83
|
+
const { webhookUrl, data } = await req.json()
|
|
84
|
+
const response = await fetch(webhookUrl, { // User controls the URL!
|
|
85
|
+
method: 'POST',
|
|
86
|
+
body: JSON.stringify(data),
|
|
87
|
+
})
|
|
88
|
+
return new Response(JSON.stringify({ status: response.status }))
|
|
89
|
+
})
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Safe Patterns
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
// SAFE: Verify JWT before using service_role
|
|
96
|
+
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
|
|
97
|
+
|
|
98
|
+
Deno.serve(async (req) => {
|
|
99
|
+
// Verify the caller's JWT
|
|
100
|
+
const authHeader = req.headers.get('Authorization')
|
|
101
|
+
if (!authHeader) {
|
|
102
|
+
return new Response('Missing authorization', { status: 401 })
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Create a user-context client to verify the token
|
|
106
|
+
const supabaseUser = createClient(
|
|
107
|
+
Deno.env.get('SUPABASE_URL')!,
|
|
108
|
+
Deno.env.get('SUPABASE_ANON_KEY')!,
|
|
109
|
+
{ global: { headers: { Authorization: authHeader } } }
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
const { data: { user }, error } = await supabaseUser.auth.getUser()
|
|
113
|
+
if (error || !user) {
|
|
114
|
+
return new Response('Invalid token', { status: 401 })
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Now use service_role for admin operations, scoped to verified user
|
|
118
|
+
const supabaseAdmin = createClient(
|
|
119
|
+
Deno.env.get('SUPABASE_URL')!,
|
|
120
|
+
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
const { data } = await supabaseAdmin
|
|
124
|
+
.from('orders')
|
|
125
|
+
.update({ status: 'processed' })
|
|
126
|
+
.eq('user_id', user.id) // user.id from verified JWT
|
|
127
|
+
|
|
128
|
+
return new Response(JSON.stringify(data))
|
|
129
|
+
})
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Search Patterns
|
|
133
|
+
|
|
134
|
+
1. Check `supabase_recon.edge_functions` for `uses_service_role: True` + `has_auth_check: False`
|
|
135
|
+
2. Read each Edge Function file (`supabase/functions/*/index.ts`)
|
|
136
|
+
3. `grep` for `SUPABASE_SERVICE_ROLE_KEY`, `Deno.env.get`, `createClient`
|
|
137
|
+
4. `grep` for `Access-Control-Allow-Origin` and CORS patterns
|
|
138
|
+
5. `grep` for `fetch(` with dynamic URLs (SSRF risk)
|
|
139
|
+
6. Check for `Authorization` header extraction and JWT verification
|
|
140
|
+
7. Look for `req.json()` fields used directly as user identity
|
|
141
|
+
|
|
142
|
+
### Severity Assessment
|
|
143
|
+
|
|
144
|
+
- **Critical**: Edge Function with service_role and no auth, performing data mutations
|
|
145
|
+
- **High**: Edge Function trusting client-supplied user IDs with service_role
|
|
146
|
+
- **High**: SSRF via user-controlled fetch URLs
|
|
147
|
+
- **Medium**: CORS wildcard with credentials
|
|
148
|
+
- **Medium**: Secrets leaked in error responses
|
|
149
|
+
- **Low**: Missing rate limiting on function invocation
|
|
150
|
+
"""
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Supabase GraphQL security detection prompt.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
SUPABASE_GRAPHQL_PROMPT = """## GraphQL Security in Supabase
|
|
6
|
+
|
|
7
|
+
### Pre-Computed Recon Available
|
|
8
|
+
|
|
9
|
+
The `supabase_recon` context already contains:
|
|
10
|
+
- `supabase_recon.graphql.introspection_enabled` -- whether introspection queries succeed as anon
|
|
11
|
+
- `supabase_recon.graphql.types_count` -- number of types exposed
|
|
12
|
+
- `supabase_recon.schema` -- PostgREST schema (compare with GraphQL for parity drift)
|
|
13
|
+
|
|
14
|
+
**Key insight: Supabase GraphQL (pg_graphql) sits on top of Postgres with RLS. However, enforcement can drift between REST and GraphQL paths, and nested queries can expose data that direct queries wouldn't.**
|
|
15
|
+
|
|
16
|
+
### What to Look For
|
|
17
|
+
|
|
18
|
+
1. **Introspection enabled in production**
|
|
19
|
+
- Introspection queries reveal the entire schema: tables, columns, types, relations
|
|
20
|
+
- Check `supabase_recon.graphql.introspection_enabled`
|
|
21
|
+
- While Supabase enables this by default, it exposes the attack surface
|
|
22
|
+
|
|
23
|
+
2. **Nested relation queries bypassing per-row checks**
|
|
24
|
+
- GraphQL allows deep nesting: `{ users { orders { payments { ... } } } }`
|
|
25
|
+
- RLS is applied per-table, but nested resolvers may not re-check ownership at each level
|
|
26
|
+
- If `users` has RLS but `orders` doesn't, querying through `users->orders` may leak order data
|
|
27
|
+
|
|
28
|
+
3. **Global node IDs reusable across viewers**
|
|
29
|
+
- pg_graphql provides global `nodeId` fields for relay-style pagination
|
|
30
|
+
- If one user discovers a `nodeId`, they might query it directly as a different user
|
|
31
|
+
- The `nodeId` encodes table and primary key, allowing targeted access
|
|
32
|
+
|
|
33
|
+
4. **REST vs GraphQL enforcement drift**
|
|
34
|
+
- Protections applied at the REST/PostgREST level may not exist in the GraphQL path
|
|
35
|
+
- Column restrictions, computed fields, or custom logic may differ between the two
|
|
36
|
+
- Test the same query via both REST and GraphQL to check parity
|
|
37
|
+
|
|
38
|
+
5. **Deep nesting for resource exhaustion**
|
|
39
|
+
- Deeply nested queries can cause expensive joins
|
|
40
|
+
- No built-in query depth limiting in pg_graphql
|
|
41
|
+
|
|
42
|
+
### Vulnerable Patterns
|
|
43
|
+
|
|
44
|
+
```graphql
|
|
45
|
+
# VULNERABLE: Introspection reveals full schema
|
|
46
|
+
{
|
|
47
|
+
__schema {
|
|
48
|
+
types {
|
|
49
|
+
name
|
|
50
|
+
fields {
|
|
51
|
+
name
|
|
52
|
+
type { name }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```graphql
|
|
60
|
+
# VULNERABLE: Deep nested query may bypass per-row checks
|
|
61
|
+
{
|
|
62
|
+
usersCollection {
|
|
63
|
+
edges {
|
|
64
|
+
node {
|
|
65
|
+
email
|
|
66
|
+
ordersCollection {
|
|
67
|
+
edges {
|
|
68
|
+
node {
|
|
69
|
+
amount
|
|
70
|
+
paymentsCollection {
|
|
71
|
+
edges {
|
|
72
|
+
node {
|
|
73
|
+
cardLast4
|
|
74
|
+
billingAddress
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Search Patterns
|
|
88
|
+
|
|
89
|
+
1. Check `supabase_recon.graphql.introspection_enabled` for immediate finding
|
|
90
|
+
2. `grep` app code for `graphql`, `/graphql/v1`, `gql` template tags
|
|
91
|
+
3. Compare `supabase_recon.schema.tables` (REST) with GraphQL introspection types
|
|
92
|
+
4. Look for nested query patterns in application code
|
|
93
|
+
5. For targeted probing: use `supabase_graphql_query` with deep nested queries on tables that have RLS gaps
|
|
94
|
+
|
|
95
|
+
### Severity Assessment
|
|
96
|
+
|
|
97
|
+
- **Critical**: Nested GraphQL queries expose sensitive data that REST + RLS would block
|
|
98
|
+
- **High**: REST vs GraphQL enforcement drift allowing bypass via GraphQL path
|
|
99
|
+
- **Medium**: Introspection enabled revealing sensitive schema structure
|
|
100
|
+
- **Medium**: Global node IDs allowing cross-user targeted access
|
|
101
|
+
- **Low**: Deep nesting without depth limits (DoS potential)
|
|
102
|
+
"""
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Supabase PostgREST and REST API vulnerability detection prompt.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
SUPABASE_POSTGREST_PROMPT = """## PostgREST / REST API Vulnerabilities in Supabase
|
|
6
|
+
|
|
7
|
+
### Pre-Computed Recon Available
|
|
8
|
+
|
|
9
|
+
The `supabase_recon` context already contains:
|
|
10
|
+
- `supabase_recon.anon_access` -- which tables the anon role can SELECT/INSERT/UPDATE/DELETE
|
|
11
|
+
- `supabase_recon.schema.columns` -- column names per table (from OpenAPI spec)
|
|
12
|
+
- `supabase_recon.schema.tables` -- all tables exposed via PostgREST
|
|
13
|
+
|
|
14
|
+
**Start by reviewing `anon_access` for tables with unexpected access permissions.** Any table where anon can read or write is a potential finding.
|
|
15
|
+
|
|
16
|
+
### What to Look For
|
|
17
|
+
|
|
18
|
+
1. **IDOR via direct row access**
|
|
19
|
+
- Tables accessible by anon where rows can be fetched by ID
|
|
20
|
+
- No ownership filter means any row is accessible: `/rest/v1/orders?id=eq.<any_id>`
|
|
21
|
+
- Check if `supabase_recon.query_patterns` shows `.from('table').select()` without `.eq('user_id', ...)`
|
|
22
|
+
|
|
23
|
+
2. **Filter abuse for data extraction**
|
|
24
|
+
- `or` filters: `?or=(user_id.eq.X,user_id.is.null)` to bypass intended scoping
|
|
25
|
+
- `ilike` filters: `?email=ilike.*@company.com` for wildcard enumeration
|
|
26
|
+
- `neq` filters: `?role=neq.admin` to probe for admin rows
|
|
27
|
+
- Combine filters to extract data column by column
|
|
28
|
+
|
|
29
|
+
3. **Relation embedding overfetch**
|
|
30
|
+
- PostgREST allows embedding related tables: `?select=*,profile(*),orders(*)`
|
|
31
|
+
- If the parent table has RLS but the embedded table doesn't, data leaks through the join
|
|
32
|
+
- Check for foreign key relationships between protected and unprotected tables
|
|
33
|
+
|
|
34
|
+
4. **Mass assignment via PATCH/POST**
|
|
35
|
+
- If anon or authenticated users can INSERT/UPDATE, they may modify unintended columns
|
|
36
|
+
- Example: setting `role`, `is_admin`, `org_id` via PATCH when only `name` was intended
|
|
37
|
+
- Check if the app uses RPC functions for writes instead of direct table access
|
|
38
|
+
|
|
39
|
+
5. **Count-based blind enumeration**
|
|
40
|
+
- `Prefer: count=exact` header returns total row count even with `limit=0`
|
|
41
|
+
- Can enumerate total records in a table without reading actual data
|
|
42
|
+
- Use `supabase_query_table` with `count: "exact"` to test
|
|
43
|
+
|
|
44
|
+
6. **Schema exposure**
|
|
45
|
+
- PostgREST OpenAPI spec reveals table names, column names, types, and foreign keys
|
|
46
|
+
- `supabase_recon.schema` already has this data -- check for sensitive column names
|
|
47
|
+
|
|
48
|
+
### Vulnerable Application Code Patterns
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// VULNERABLE: No ownership filter, any user can read all orders
|
|
52
|
+
const { data } = await supabase
|
|
53
|
+
.from('orders')
|
|
54
|
+
.select('*')
|
|
55
|
+
|
|
56
|
+
// VULNERABLE: Client-controlled filter that can be bypassed
|
|
57
|
+
const { data } = await supabase
|
|
58
|
+
.from('users')
|
|
59
|
+
.select('*')
|
|
60
|
+
.eq('id', params.id) // User controls the ID
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// VULNERABLE: Embedding exposes related data
|
|
65
|
+
const { data } = await supabase
|
|
66
|
+
.from('posts')
|
|
67
|
+
.select('*, author:users(*), comments(*)')
|
|
68
|
+
// If users table has weaker RLS than posts, author data leaks
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Safe Patterns
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
// SAFE: Server-side ownership filter using authenticated user
|
|
75
|
+
const { data: { user } } = await supabase.auth.getUser()
|
|
76
|
+
const { data } = await supabase
|
|
77
|
+
.from('orders')
|
|
78
|
+
.select('*')
|
|
79
|
+
.eq('user_id', user.id)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Search Patterns
|
|
83
|
+
|
|
84
|
+
1. Check `supabase_recon.anon_access` for tables with `select: True` or `insert: True`
|
|
85
|
+
2. Check `supabase_recon.schema.columns` for sensitive column names (email, password, ssn, token, secret)
|
|
86
|
+
3. `grep` app code for `.from(` calls without subsequent `.eq('user_id'` or ownership filters
|
|
87
|
+
4. Look for `select('*,` patterns that embed related tables
|
|
88
|
+
5. For targeted probing: use `supabase_query_table` with filter combinations to test data extraction
|
|
89
|
+
6. For write testing: use `supabase_mutate_table` to test mass assignment
|
|
90
|
+
|
|
91
|
+
### Severity Assessment
|
|
92
|
+
|
|
93
|
+
- **Critical**: Anon can read sensitive data (PII, financial) from tables with no RLS
|
|
94
|
+
- **High**: Anon can write to tables (INSERT/UPDATE), enabling data manipulation
|
|
95
|
+
- **High**: Filter abuse allows extraction of data that should be scoped per-user
|
|
96
|
+
- **Medium**: Schema exposure reveals sensitive table/column structure
|
|
97
|
+
- **Medium**: Count-based enumeration reveals record counts
|
|
98
|
+
- **Low**: Non-sensitive public data accessible (may be intentional)
|
|
99
|
+
"""
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Supabase Realtime security detection prompt.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
SUPABASE_REALTIME_PROMPT = """## Realtime Security in Supabase
|
|
6
|
+
|
|
7
|
+
### Pre-Computed Recon Available
|
|
8
|
+
|
|
9
|
+
The `supabase_recon` context already contains:
|
|
10
|
+
- `supabase_recon.anon_access` -- tables accessible by anon (if anon can SELECT, realtime changes likely also leak)
|
|
11
|
+
- `supabase_recon.rls_policies` -- RLS status per table (realtime respects RLS for postgres_changes)
|
|
12
|
+
|
|
13
|
+
**Key insight: Realtime postgres_changes subscriptions are governed by the same RLS policies as direct table access. If a table has no RLS or permissive RLS, realtime will leak every change to any subscriber.**
|
|
14
|
+
|
|
15
|
+
### What to Look For
|
|
16
|
+
|
|
17
|
+
1. **Subscriptions to tables without RLS**
|
|
18
|
+
- `supabase.channel('*').on('postgres_changes', { table: 'orders' }, ...)` on a table without RLS
|
|
19
|
+
- Every INSERT/UPDATE/DELETE on that table is broadcast to all subscribers including anon
|
|
20
|
+
- Cross-reference: if `anon_access[table].select` is True, realtime changes are also exposed
|
|
21
|
+
|
|
22
|
+
2. **Broadcast/presence channels without authentication**
|
|
23
|
+
- Broadcast and presence channels don't go through RLS
|
|
24
|
+
- Channel names derived from guessable IDs: `room:${userId}`, `org:${orgId}`
|
|
25
|
+
- Any client can join any channel name and receive/send messages
|
|
26
|
+
|
|
27
|
+
3. **Sensitive data in realtime payloads**
|
|
28
|
+
- Even with RLS, the `NEW` and `OLD` records in change events include all columns
|
|
29
|
+
- If the table has sensitive columns (passwords, tokens, secrets), they leak via realtime
|
|
30
|
+
- Check if the subscription uses column filtering
|
|
31
|
+
|
|
32
|
+
4. **Cross-room join/publish**
|
|
33
|
+
- Broadcast channels: client can publish to any channel they can join
|
|
34
|
+
- If channel names encode authorization (e.g., `admin-updates`), any client can subscribe
|
|
35
|
+
|
|
36
|
+
### Vulnerable Patterns
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
// VULNERABLE: Subscribing to table without RLS
|
|
40
|
+
supabase
|
|
41
|
+
.channel('orders-changes')
|
|
42
|
+
.on('postgres_changes',
|
|
43
|
+
{ event: '*', schema: 'public', table: 'orders' },
|
|
44
|
+
(payload) => {
|
|
45
|
+
console.log('Change received:', payload)
|
|
46
|
+
// Receives ALL changes to orders table from ALL users
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
.subscribe()
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// VULNERABLE: Guessable channel name without auth verification
|
|
54
|
+
supabase
|
|
55
|
+
.channel(`user:${otherUserId}`) // Can join any user's channel
|
|
56
|
+
.on('broadcast', { event: 'notification' }, (payload) => {
|
|
57
|
+
// Receives another user's notifications
|
|
58
|
+
})
|
|
59
|
+
.subscribe()
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Safe Patterns
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
// SAFE: Table has proper RLS, so only authorized changes are received
|
|
66
|
+
// (Assuming orders table has: USING (auth.uid() = user_id) policy)
|
|
67
|
+
supabase
|
|
68
|
+
.channel('my-orders')
|
|
69
|
+
.on('postgres_changes',
|
|
70
|
+
{ event: '*', schema: 'public', table: 'orders',
|
|
71
|
+
filter: `user_id=eq.${user.id}` },
|
|
72
|
+
(payload) => {
|
|
73
|
+
// Only receives changes for current user's orders
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
.subscribe()
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Search Patterns
|
|
80
|
+
|
|
81
|
+
1. Check `supabase_recon.rls_policies.tables_without_rls` -- any table subscribed to via realtime is vulnerable
|
|
82
|
+
2. `grep` app code for `.channel(`, `.on('postgres_changes'`, `.on('broadcast'`, `.subscribe(`
|
|
83
|
+
3. Check channel name patterns for guessable IDs
|
|
84
|
+
4. Cross-reference subscribed tables with `supabase_recon.anon_access`
|
|
85
|
+
5. Look for `filter:` parameter in postgres_changes subscriptions (reduces exposure but doesn't replace RLS)
|
|
86
|
+
|
|
87
|
+
### Severity Assessment
|
|
88
|
+
|
|
89
|
+
- **Critical**: Realtime subscription on sensitive table without RLS, leaking all changes to anon
|
|
90
|
+
- **High**: Broadcast/presence channels with guessable names exposing user-specific data
|
|
91
|
+
- **Medium**: Realtime subscription includes sensitive columns even with RLS
|
|
92
|
+
- **Low**: Realtime on intentionally public data (e.g., public chat, live scores)
|
|
93
|
+
"""
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Supabase Row Level Security (RLS) misconfiguration detection prompt.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
SUPABASE_RLS_PROMPT = """## Row Level Security (RLS) Misconfigurations in Supabase
|
|
6
|
+
|
|
7
|
+
### Pre-Computed Recon Available
|
|
8
|
+
|
|
9
|
+
The `supabase_recon` context already contains the results of deterministic RLS analysis:
|
|
10
|
+
- `supabase_recon.rls_policies.tables_without_rls` -- tables that have NO RLS enabled in migrations
|
|
11
|
+
- `supabase_recon.rls_policies.tables` -- all tables with their RLS status and policy details
|
|
12
|
+
- `supabase_recon.anon_access` -- runtime test results showing which tables the anon role can actually read/write
|
|
13
|
+
|
|
14
|
+
**Start by cross-referencing these two datasets.** A table with no RLS in migrations AND `select: True` in `anon_access` is an instant critical finding.
|
|
15
|
+
|
|
16
|
+
### What to Look For
|
|
17
|
+
|
|
18
|
+
1. **Tables without RLS enabled**
|
|
19
|
+
- Every non-public table must have `ALTER TABLE ... ENABLE ROW LEVEL SECURITY`
|
|
20
|
+
- Absence means the table is wide open to any role with access
|
|
21
|
+
|
|
22
|
+
2. **Overly permissive policies**
|
|
23
|
+
- `using (true)` or `with check (true)` grants unrestricted access
|
|
24
|
+
- Policies that don't reference `auth.uid()` or a tenant column
|
|
25
|
+
|
|
26
|
+
3. **Per-operation gaps**
|
|
27
|
+
- SELECT policy exists but UPDATE/DELETE/INSERT policies are missing
|
|
28
|
+
- A table may be read-protected but write-open (or vice versa)
|
|
29
|
+
|
|
30
|
+
4. **Policies trusting client-supplied data**
|
|
31
|
+
- Policy checks `user_id` column (which can be set by the client on INSERT) instead of `auth.uid()` from the JWT
|
|
32
|
+
- Example: `using (user_id = auth.uid())` on SELECT is fine, but `with check (user_id = auth.uid())` on INSERT trusts whatever the client sends as `user_id` if not also constrained
|
|
33
|
+
|
|
34
|
+
5. **Missing tenant/org constraints**
|
|
35
|
+
- Multi-tenant apps must scope every policy to `org_id`/`tenant_id`
|
|
36
|
+
- Missing tenant constraint allows cross-tenant data access
|
|
37
|
+
|
|
38
|
+
6. **Complex join inference**
|
|
39
|
+
- Even with RLS, count-based queries (`Prefer: count=exact`) can leak information about rows the user shouldn't see
|
|
40
|
+
- Policies applied after filters can allow inference via response timing or counts
|
|
41
|
+
|
|
42
|
+
### Vulnerable Migration Patterns
|
|
43
|
+
|
|
44
|
+
```sql
|
|
45
|
+
-- VULNERABLE: Table created with NO RLS
|
|
46
|
+
CREATE TABLE orders (
|
|
47
|
+
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
|
|
48
|
+
user_id uuid REFERENCES auth.users(id),
|
|
49
|
+
amount numeric,
|
|
50
|
+
status text
|
|
51
|
+
);
|
|
52
|
+
-- Missing: ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
|
|
53
|
+
-- Missing: CREATE POLICY ... ON orders ...
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```sql
|
|
57
|
+
-- VULNERABLE: RLS enabled but permit-all policy
|
|
58
|
+
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
|
|
59
|
+
CREATE POLICY "allow_all" ON documents FOR ALL USING (true);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```sql
|
|
63
|
+
-- VULNERABLE: SELECT policy exists, but no INSERT/UPDATE/DELETE policies
|
|
64
|
+
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
|
|
65
|
+
CREATE POLICY "users_read_own" ON profiles FOR SELECT USING (auth.uid() = id);
|
|
66
|
+
-- No INSERT/UPDATE/DELETE policies = default deny, but could be intentionally missing
|
|
67
|
+
-- or could be a gap if the app needs write access
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```sql
|
|
71
|
+
-- VULNERABLE: Policy trusts client-supplied user_id on INSERT
|
|
72
|
+
CREATE POLICY "users_insert" ON posts FOR INSERT
|
|
73
|
+
WITH CHECK (user_id = auth.uid());
|
|
74
|
+
-- If user_id is in the INSERT payload, client can set user_id = auth.uid() trivially
|
|
75
|
+
-- But what about other columns like org_id, role, etc.?
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Safe Patterns
|
|
79
|
+
|
|
80
|
+
```sql
|
|
81
|
+
-- SAFE: Proper RLS with per-operation policies
|
|
82
|
+
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
|
|
83
|
+
CREATE POLICY "users_read_own_orders" ON orders FOR SELECT
|
|
84
|
+
USING (auth.uid() = user_id);
|
|
85
|
+
CREATE POLICY "users_insert_own_orders" ON orders FOR INSERT
|
|
86
|
+
WITH CHECK (auth.uid() = user_id);
|
|
87
|
+
CREATE POLICY "users_update_own_orders" ON orders FOR UPDATE
|
|
88
|
+
USING (auth.uid() = user_id);
|
|
89
|
+
CREATE POLICY "users_delete_own_orders" ON orders FOR DELETE
|
|
90
|
+
USING (auth.uid() = user_id);
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Search Patterns
|
|
94
|
+
|
|
95
|
+
1. Check `supabase_recon.rls_policies.tables_without_rls` for immediate findings
|
|
96
|
+
2. Cross-reference with `supabase_recon.anon_access` to confirm runtime exposure
|
|
97
|
+
3. `grep` migration files for `using (true)` and `with check (true)`
|
|
98
|
+
4. Check policies for `auth.uid()` references -- policies without it are suspicious
|
|
99
|
+
5. Look for tables with SELECT policy but missing UPDATE/DELETE/INSERT policies
|
|
100
|
+
6. For targeted probing: use `supabase_query_table` with filters like `or=(user_id.eq.X,user_id.is.null)` to test policy enforcement
|
|
101
|
+
|
|
102
|
+
### Severity Assessment
|
|
103
|
+
|
|
104
|
+
- **Critical**: Table without RLS + anon can read/write data at runtime (confirmed by `anon_access`)
|
|
105
|
+
- **Critical**: Sensitive table (users, payments, PII) with `using (true)` policy
|
|
106
|
+
- **High**: RLS enabled but policy is overly permissive or missing for some operations
|
|
107
|
+
- **High**: Policy trusts client-supplied columns instead of JWT context
|
|
108
|
+
- **Medium**: Missing tenant isolation in multi-tenant app
|
|
109
|
+
- **Low**: RLS gap on non-sensitive, intentionally public data
|
|
110
|
+
"""
|