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,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Supabase RPC function security detection prompt.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
SUPABASE_RPC_PROMPT = """## RPC Function Security in Supabase
|
|
6
|
+
|
|
7
|
+
### Pre-Computed Recon Available
|
|
8
|
+
|
|
9
|
+
The `supabase_recon` context already contains:
|
|
10
|
+
- `supabase_recon.rpc_functions` -- all SQL functions found in migrations with their security mode (DEFINER/INVOKER), parameters, and whether they reference `auth.uid()`
|
|
11
|
+
- `supabase_recon.rpc_access` -- runtime test showing which RPC functions are callable by anon (with status codes)
|
|
12
|
+
- `supabase_recon.schema.functions` -- functions exposed via PostgREST OpenAPI spec
|
|
13
|
+
|
|
14
|
+
**Critical cross-reference: a function with `security_mode: "DEFINER"` + `has_auth_uid_check: False` + `callable: True` in `rpc_access` is likely a privilege escalation vector.**
|
|
15
|
+
|
|
16
|
+
### What to Look For
|
|
17
|
+
|
|
18
|
+
1. **SECURITY DEFINER without ownership checks**
|
|
19
|
+
- `SECURITY DEFINER` functions execute with the privileges of the function owner (usually `postgres`), bypassing RLS entirely
|
|
20
|
+
- If the function doesn't internally verify `auth.uid()`, any caller (including anon) can access/modify any data
|
|
21
|
+
- This is the most common and dangerous Supabase misconfiguration
|
|
22
|
+
|
|
23
|
+
2. **Unsafe search_path**
|
|
24
|
+
- Functions with `search_path` set to `public` or not set at all
|
|
25
|
+
- Malicious users could create objects in the public schema that the function resolves instead of intended objects
|
|
26
|
+
|
|
27
|
+
3. **Client-supplied IDs trusted over JWT**
|
|
28
|
+
- Function parameters like `p_user_id` used directly in queries instead of `auth.uid()`
|
|
29
|
+
- Caller can pass any user ID to access other users' data
|
|
30
|
+
|
|
31
|
+
4. **Anon-callable sensitive functions**
|
|
32
|
+
- Functions that should require authentication but are callable by the anon role
|
|
33
|
+
- Check `supabase_recon.rpc_access` for functions with `callable: True`
|
|
34
|
+
|
|
35
|
+
### Vulnerable Patterns
|
|
36
|
+
|
|
37
|
+
```sql
|
|
38
|
+
-- VULNERABLE: SECURITY DEFINER with no auth check
|
|
39
|
+
CREATE OR REPLACE FUNCTION get_user_stats(p_user_id uuid)
|
|
40
|
+
RETURNS json
|
|
41
|
+
LANGUAGE plpgsql
|
|
42
|
+
SECURITY DEFINER
|
|
43
|
+
AS $$
|
|
44
|
+
BEGIN
|
|
45
|
+
-- Trusts client-supplied user_id, bypasses RLS
|
|
46
|
+
RETURN (SELECT row_to_json(u) FROM users u WHERE u.id = p_user_id);
|
|
47
|
+
END;
|
|
48
|
+
$$;
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```sql
|
|
52
|
+
-- VULNERABLE: Unsafe search_path
|
|
53
|
+
CREATE OR REPLACE FUNCTION admin_action()
|
|
54
|
+
RETURNS void
|
|
55
|
+
LANGUAGE plpgsql
|
|
56
|
+
SECURITY DEFINER
|
|
57
|
+
SET search_path = public -- Attacker can shadow objects in public schema
|
|
58
|
+
AS $$
|
|
59
|
+
BEGIN
|
|
60
|
+
-- ...
|
|
61
|
+
END;
|
|
62
|
+
$$;
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
```sql
|
|
66
|
+
-- VULNERABLE: No ownership check, any user can call
|
|
67
|
+
CREATE OR REPLACE FUNCTION delete_account(p_user_id uuid)
|
|
68
|
+
RETURNS void
|
|
69
|
+
LANGUAGE plpgsql
|
|
70
|
+
SECURITY DEFINER
|
|
71
|
+
AS $$
|
|
72
|
+
BEGIN
|
|
73
|
+
DELETE FROM users WHERE id = p_user_id; -- Any anon user can delete any account!
|
|
74
|
+
END;
|
|
75
|
+
$$;
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Safe Patterns
|
|
79
|
+
|
|
80
|
+
```sql
|
|
81
|
+
-- SAFE: SECURITY INVOKER respects caller's RLS
|
|
82
|
+
CREATE OR REPLACE FUNCTION get_my_stats()
|
|
83
|
+
RETURNS json
|
|
84
|
+
LANGUAGE plpgsql
|
|
85
|
+
SECURITY INVOKER
|
|
86
|
+
AS $$
|
|
87
|
+
BEGIN
|
|
88
|
+
RETURN (SELECT row_to_json(u) FROM users u WHERE u.id = auth.uid());
|
|
89
|
+
END;
|
|
90
|
+
$$;
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
```sql
|
|
94
|
+
-- SAFE: DEFINER with explicit auth check and safe search_path
|
|
95
|
+
CREATE OR REPLACE FUNCTION admin_get_user(p_user_id uuid)
|
|
96
|
+
RETURNS json
|
|
97
|
+
LANGUAGE plpgsql
|
|
98
|
+
SECURITY DEFINER
|
|
99
|
+
SET search_path = ''
|
|
100
|
+
AS $$
|
|
101
|
+
BEGIN
|
|
102
|
+
-- Verify caller is admin
|
|
103
|
+
IF NOT EXISTS (SELECT 1 FROM auth.users WHERE id = auth.uid() AND raw_user_meta_data->>'role' = 'admin') THEN
|
|
104
|
+
RAISE EXCEPTION 'Unauthorized';
|
|
105
|
+
END IF;
|
|
106
|
+
RETURN (SELECT row_to_json(u) FROM public.users u WHERE u.id = p_user_id);
|
|
107
|
+
END;
|
|
108
|
+
$$;
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Search Patterns
|
|
112
|
+
|
|
113
|
+
1. Check `supabase_recon.rpc_functions` for `security_mode: "DEFINER"` + `has_auth_uid_check: False`
|
|
114
|
+
2. Cross-reference with `supabase_recon.rpc_access` for `callable: True`
|
|
115
|
+
3. `grep` migrations for `SECURITY DEFINER` to find all definer functions
|
|
116
|
+
4. `grep` for `search_path` settings in function definitions
|
|
117
|
+
5. Read function bodies to check for `auth.uid()` usage
|
|
118
|
+
6. For targeted probing: use `supabase_call_rpc` with foreign user IDs to test horizontal access
|
|
119
|
+
|
|
120
|
+
### Severity Assessment
|
|
121
|
+
|
|
122
|
+
- **Critical**: SECURITY DEFINER function callable by anon with no auth check, accessing sensitive data
|
|
123
|
+
- **High**: SECURITY DEFINER function callable by authenticated users but trusting client-supplied user_id
|
|
124
|
+
- **High**: Function with unsafe search_path allowing object shadowing
|
|
125
|
+
- **Medium**: Function missing tenant isolation checks
|
|
126
|
+
- **Low**: SECURITY INVOKER function with minor input validation gaps
|
|
127
|
+
"""
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Supabase Storage security detection prompt.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
SUPABASE_STORAGE_PROMPT = """## Storage Security in Supabase
|
|
6
|
+
|
|
7
|
+
### Pre-Computed Recon Available
|
|
8
|
+
|
|
9
|
+
The `supabase_recon` context already contains:
|
|
10
|
+
- `supabase_recon.storage_policies.buckets` -- all storage buckets found in migrations with public/private status
|
|
11
|
+
- `supabase_recon.storage_policies.policies` -- RLS policies on `storage.objects`
|
|
12
|
+
- `supabase_recon.storage_access` -- runtime test showing which buckets are listable by anon
|
|
13
|
+
|
|
14
|
+
**Cross-reference: a bucket marked `public: True` in migrations + `listable: True` in `storage_access` that contains sensitive files is a finding.**
|
|
15
|
+
|
|
16
|
+
### What to Look For
|
|
17
|
+
|
|
18
|
+
1. **Public buckets with sensitive content**
|
|
19
|
+
- Buckets marked as public serve files without authentication
|
|
20
|
+
- If the bucket holds user uploads, documents, or exports, this is a data leak
|
|
21
|
+
- Check bucket names: `documents`, `exports`, `reports`, `private`, `internal` suggest sensitive content
|
|
22
|
+
|
|
23
|
+
2. **Missing storage policies**
|
|
24
|
+
- `storage.objects` has RLS just like regular tables
|
|
25
|
+
- Without policies, default behavior depends on bucket public/private setting
|
|
26
|
+
- Private buckets without policies may still be accessible via signed URLs that never expire
|
|
27
|
+
|
|
28
|
+
3. **Signed URL reuse across tenants/users**
|
|
29
|
+
- Signed URLs generated for one user may be valid for any user
|
|
30
|
+
- Check if URL generation includes user-specific scoping
|
|
31
|
+
- Look for `createSignedUrl` calls without tenant/user path prefixes
|
|
32
|
+
|
|
33
|
+
4. **Content-type abuse**
|
|
34
|
+
- Uploading HTML or SVG files that are served as `text/html` or `image/svg+xml`
|
|
35
|
+
- These can contain JavaScript and enable XSS attacks
|
|
36
|
+
- Check for `Content-Type` validation on upload and `X-Content-Type-Options: nosniff` on download
|
|
37
|
+
|
|
38
|
+
5. **Path confusion**
|
|
39
|
+
- Mixed case, URL-encoding, or `..` segments may bypass client-side validation
|
|
40
|
+
- Server and client may normalize paths differently
|
|
41
|
+
- Users might access files outside their intended directory
|
|
42
|
+
|
|
43
|
+
6. **Bucket listing exposure**
|
|
44
|
+
- `storage.from('bucket').list()` without authentication exposes file inventory
|
|
45
|
+
- Even if individual files are protected, knowing the file names/paths is information disclosure
|
|
46
|
+
|
|
47
|
+
### Vulnerable Patterns
|
|
48
|
+
|
|
49
|
+
```sql
|
|
50
|
+
-- VULNERABLE: Public bucket for sensitive documents
|
|
51
|
+
INSERT INTO storage.buckets (id, name, public) VALUES ('documents', 'documents', true);
|
|
52
|
+
-- No policies on storage.objects for this bucket
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// VULNERABLE: Generating signed URLs without user scoping
|
|
57
|
+
const { data } = await supabase.storage
|
|
58
|
+
.from('documents')
|
|
59
|
+
.createSignedUrl('reports/financial-report.pdf', 3600)
|
|
60
|
+
// This URL works for anyone who has it, no user verification
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
// VULNERABLE: No content-type validation on upload
|
|
65
|
+
const { error } = await supabase.storage
|
|
66
|
+
.from('avatars')
|
|
67
|
+
.upload(`${userId}/avatar`, file)
|
|
68
|
+
// User could upload malicious.html as their "avatar"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Safe Patterns
|
|
72
|
+
|
|
73
|
+
```sql
|
|
74
|
+
-- SAFE: Private bucket with user-scoped policies
|
|
75
|
+
INSERT INTO storage.buckets (id, name, public) VALUES ('documents', 'documents', false);
|
|
76
|
+
CREATE POLICY "users_access_own_documents" ON storage.objects
|
|
77
|
+
FOR ALL USING (bucket_id = 'documents' AND auth.uid()::text = (storage.foldername(name))[1]);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// SAFE: User-scoped upload path with content-type validation
|
|
82
|
+
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp']
|
|
83
|
+
if (!allowedTypes.includes(file.type)) throw new Error('Invalid file type')
|
|
84
|
+
|
|
85
|
+
const { error } = await supabase.storage
|
|
86
|
+
.from('avatars')
|
|
87
|
+
.upload(`${user.id}/avatar.${ext}`, file, {
|
|
88
|
+
contentType: file.type,
|
|
89
|
+
upsert: true,
|
|
90
|
+
})
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Search Patterns
|
|
94
|
+
|
|
95
|
+
1. Check `supabase_recon.storage_policies.buckets` for `public: True` buckets
|
|
96
|
+
2. Cross-reference with `supabase_recon.storage_access` for `listable: True` buckets
|
|
97
|
+
3. `grep` migrations for `storage.buckets` and `storage.objects` to find all bucket/policy definitions
|
|
98
|
+
4. `grep` app code for `createSignedUrl`, `getPublicUrl`, `.upload(` to find storage usage
|
|
99
|
+
5. Check for content-type validation near upload calls
|
|
100
|
+
6. For targeted probing: use `supabase_probe_storage` to list specific bucket paths
|
|
101
|
+
|
|
102
|
+
### Severity Assessment
|
|
103
|
+
|
|
104
|
+
- **Critical**: Public bucket containing PII, financial data, or credentials
|
|
105
|
+
- **High**: Private bucket with missing storage policies allowing unauthorized access
|
|
106
|
+
- **High**: Signed URLs reusable across users/tenants for sensitive files
|
|
107
|
+
- **Medium**: Content-type abuse potential (HTML/SVG uploads served as text/html)
|
|
108
|
+
- **Medium**: Bucket listing exposes file inventory
|
|
109
|
+
- **Low**: Public bucket for intentionally public assets (logos, public images)
|
|
110
|
+
"""
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Supabase tenant isolation security detection prompt.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
SUPABASE_TENANT_ISOLATION_PROMPT = """## Tenant Isolation in Supabase
|
|
6
|
+
|
|
7
|
+
### Pre-Computed Recon Available
|
|
8
|
+
|
|
9
|
+
The `supabase_recon` context already contains:
|
|
10
|
+
- `supabase_recon.query_patterns` -- all `.from()` calls with `has_ownership_filter` and `has_tenant_filter` flags
|
|
11
|
+
- `supabase_recon.rls_policies` -- RLS policies and whether they reference tenant columns
|
|
12
|
+
- `supabase_recon.anon_access` -- which tables anon can access (cross-tenant probing surface)
|
|
13
|
+
|
|
14
|
+
**Key insight: in multi-tenant Supabase apps, every query AND every RLS policy must be scoped by `tenant_id`/`org_id`/`team_id` derived from the JWT, NOT from client input.**
|
|
15
|
+
|
|
16
|
+
### What to Look For
|
|
17
|
+
|
|
18
|
+
1. **Queries not scoped by tenant**
|
|
19
|
+
- Application code querying tables without filtering by `org_id`/`tenant_id`
|
|
20
|
+
- Check `supabase_recon.query_patterns` for `has_tenant_filter: False`
|
|
21
|
+
- Even with RLS, application-level scoping is defense in depth
|
|
22
|
+
|
|
23
|
+
2. **RLS policies trusting client-supplied tenant context**
|
|
24
|
+
- Policy uses `org_id` column value that the client can set on INSERT
|
|
25
|
+
- Instead of: `USING (org_id = (auth.jwt() -> 'app_metadata' ->> 'org_id')::uuid)`
|
|
26
|
+
- Wrong: `USING (org_id = org_id)` (tautology, always true)
|
|
27
|
+
|
|
28
|
+
3. **Tenant selector inconsistent with JWT**
|
|
29
|
+
- App determines tenant from subdomain, header, or path parameter
|
|
30
|
+
- But JWT encodes a different tenant claim
|
|
31
|
+
- User from tenant A can switch subdomain to tenant B and access their data
|
|
32
|
+
|
|
33
|
+
4. **Export/report endpoints outside caller scope**
|
|
34
|
+
- Endpoints that generate CSV exports, PDF reports, or aggregations
|
|
35
|
+
- If the query doesn't filter by tenant from JWT, it may include cross-tenant data
|
|
36
|
+
- Check server-side code for export/report generation
|
|
37
|
+
|
|
38
|
+
5. **Cross-tenant probing via filters**
|
|
39
|
+
- Using `or` filters: `?or=(org_id.eq.other_org,org_id.is.null)`
|
|
40
|
+
- Filtering by another org's ID: `?org_id=eq.foreign_org_id`
|
|
41
|
+
- If RLS doesn't enforce tenant isolation, these queries return cross-tenant data
|
|
42
|
+
|
|
43
|
+
### Vulnerable Patterns
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
// VULNERABLE: No tenant scoping on query
|
|
47
|
+
const { data } = await supabase
|
|
48
|
+
.from('projects')
|
|
49
|
+
.select('*')
|
|
50
|
+
// Missing: .eq('org_id', user.org_id)
|
|
51
|
+
// Returns ALL projects across ALL tenants
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```sql
|
|
55
|
+
-- VULNERABLE: RLS policy without tenant constraint
|
|
56
|
+
CREATE POLICY "users_access" ON projects
|
|
57
|
+
FOR SELECT USING (auth.uid() = created_by);
|
|
58
|
+
-- Missing org_id check -- user can see projects from other orgs
|
|
59
|
+
-- if they happen to be created_by (unlikely but possible in edge cases)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
// VULNERABLE: Tenant from URL, not JWT
|
|
64
|
+
export async function GET(req, { params }) {
|
|
65
|
+
const orgId = params.orgId // From URL path, client-controlled!
|
|
66
|
+
const { data } = await supabase
|
|
67
|
+
.from('projects')
|
|
68
|
+
.select('*')
|
|
69
|
+
.eq('org_id', orgId) // Client picks which org to access
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```sql
|
|
74
|
+
-- VULNERABLE: Policy with tautological check
|
|
75
|
+
CREATE POLICY "org_access" ON documents
|
|
76
|
+
FOR SELECT USING (org_id = org_id); -- Always true!
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Safe Patterns
|
|
80
|
+
|
|
81
|
+
```sql
|
|
82
|
+
-- SAFE: RLS policy scoped to tenant from JWT
|
|
83
|
+
CREATE POLICY "tenant_isolation" ON projects
|
|
84
|
+
FOR ALL USING (
|
|
85
|
+
org_id = (auth.jwt() -> 'app_metadata' ->> 'org_id')::uuid
|
|
86
|
+
);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
// SAFE: Tenant derived from verified JWT, not client input
|
|
91
|
+
const { data: { user } } = await supabase.auth.getUser()
|
|
92
|
+
const orgId = user.app_metadata.org_id // From verified JWT
|
|
93
|
+
|
|
94
|
+
const { data } = await supabase
|
|
95
|
+
.from('projects')
|
|
96
|
+
.select('*')
|
|
97
|
+
.eq('org_id', orgId)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Search Patterns
|
|
101
|
+
|
|
102
|
+
1. Check `supabase_recon.query_patterns` for `has_tenant_filter: False` on multi-tenant tables
|
|
103
|
+
2. `grep` for `org_id`, `tenant_id`, `team_id`, `workspace_id` in RLS policies
|
|
104
|
+
3. `grep` app code for `.eq('org_id'` to see how tenant is derived (from JWT vs URL/header)
|
|
105
|
+
4. Look for export/report endpoints (`/api/export`, `/api/report`, `csv`, `download`)
|
|
106
|
+
5. Check RLS policies for tautological conditions
|
|
107
|
+
6. For targeted probing: use `supabase_query_table` with `org_id=eq.foreign-org` to test isolation
|
|
108
|
+
|
|
109
|
+
### Severity Assessment
|
|
110
|
+
|
|
111
|
+
- **Critical**: Cross-tenant data access confirmed via runtime probing
|
|
112
|
+
- **Critical**: Sensitive data (financial, PII) accessible across tenants
|
|
113
|
+
- **High**: RLS policies missing tenant constraints in multi-tenant app
|
|
114
|
+
- **High**: Tenant derived from client input (URL/header) instead of JWT
|
|
115
|
+
- **Medium**: Export endpoints generating unscoped reports
|
|
116
|
+
- **Medium**: Application queries missing tenant filters (defense in depth gap)
|
|
117
|
+
- **Low**: Tenant isolation relies solely on RLS (no application-level check)
|
|
118
|
+
"""
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Validator agent prompt template.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
VALIDATOR_PROMPT = """You are the Validator agent for OpenHack Agent. Your job is to confirm whether potential vulnerabilities are ACTUALLY EXPLOITABLE -- not just theoretically possible.
|
|
6
|
+
|
|
7
|
+
{project_context}
|
|
8
|
+
|
|
9
|
+
## Thinking Style - CRITICAL
|
|
10
|
+
|
|
11
|
+
You MUST think out loud before EVERY tool call. Validation requires careful reasoning.
|
|
12
|
+
|
|
13
|
+
Before EACH action, explain:
|
|
14
|
+
1. What am I trying to verify?
|
|
15
|
+
2. What would confirm or disprove this vulnerability?
|
|
16
|
+
3. What security controls might I have missed?
|
|
17
|
+
|
|
18
|
+
Example thought process:
|
|
19
|
+
"Finding #1 claims the 'secrets' table is readable by anon with actual API keys in the response. Let me independently query this table. If I get actual rows containing api_key values, this is confirmed. If I get 0 rows, RLS is filtering and this is a false positive."
|
|
20
|
+
|
|
21
|
+
Another example:
|
|
22
|
+
"Finding #2 claims write access to 'public_notes'. The hunter reported `insert: true` but didn't prove it. Let me run the canary protocol -- INSERT a __openhack_test_ row, SELECT it back, UPDATE it, DELETE it. Only if I can complete this cycle is write access confirmed."
|
|
23
|
+
|
|
24
|
+
ALWAYS explain your reasoning. The user needs to understand why each finding is valid or a false positive.
|
|
25
|
+
|
|
26
|
+
## Core Principle: VALIDATION = ACTUAL EXPLOITATION
|
|
27
|
+
|
|
28
|
+
A finding is ONLY confirmed when you have **response data proving it**:
|
|
29
|
+
- **Data leak confirmed** = Your query returned actual rows with sensitive data
|
|
30
|
+
- **Write access confirmed** = Your canary INSERT was read back (the row exists)
|
|
31
|
+
- **RPC abuse confirmed** = Your RPC call returned actual sensitive data
|
|
32
|
+
- **Storage leak confirmed** = You read actual file contents from a bucket
|
|
33
|
+
|
|
34
|
+
A finding is a **FALSE POSITIVE** when:
|
|
35
|
+
- SELECT returned 200 but 0 rows (RLS is filtering correctly)
|
|
36
|
+
- Mutation returned 204 but `affected_count: 0` and no rows in response body
|
|
37
|
+
- RPC returned an error or empty result
|
|
38
|
+
- The "sensitive" data is actually public/non-sensitive
|
|
39
|
+
|
|
40
|
+
## Your Mission
|
|
41
|
+
|
|
42
|
+
For each potential vulnerability from Hunter:
|
|
43
|
+
1. **Independently exploit it** - Use tools to actually extract data or modify rows
|
|
44
|
+
2. **Prove it with evidence** - Show actual response data, not just status codes
|
|
45
|
+
3. **Run canary protocol** for write access claims (see below)
|
|
46
|
+
4. **Generate practical PoC** - Python (`requests`) that anyone can copy-paste to reproduce
|
|
47
|
+
5. **Suggest fix** - Recommend how to remediate
|
|
48
|
+
|
|
49
|
+
## Canary Protocol for Write Access Validation
|
|
50
|
+
|
|
51
|
+
When a finding claims write access (INSERT/UPDATE/DELETE), you MUST prove it with the canary protocol.
|
|
52
|
+
|
|
53
|
+
**CRITICAL RULE: NEVER modify or delete data you did not create. Only operate on canary rows with `__openhack_test_` markers.**
|
|
54
|
+
|
|
55
|
+
### Step-by-step:
|
|
56
|
+
|
|
57
|
+
1. **INSERT a canary row:**
|
|
58
|
+
```
|
|
59
|
+
supabase_http_request(
|
|
60
|
+
method="POST",
|
|
61
|
+
path="/rest/v1/<table>",
|
|
62
|
+
headers={{"Prefer": "return=representation"}},
|
|
63
|
+
body={{"name": "__openhack_test_probe", "email": "__openhack_test_@example.com"}}
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
- Check response: if body contains the inserted row → INSERT works
|
|
67
|
+
|
|
68
|
+
2. **SELECT the canary back:**
|
|
69
|
+
```
|
|
70
|
+
supabase_http_request(
|
|
71
|
+
method="GET",
|
|
72
|
+
path="/rest/v1/<table>?name=eq.__openhack_test_probe"
|
|
73
|
+
)
|
|
74
|
+
```
|
|
75
|
+
- If the canary appears → READ + WRITE confirmed
|
|
76
|
+
|
|
77
|
+
3. **UPDATE the canary:**
|
|
78
|
+
```
|
|
79
|
+
supabase_http_request(
|
|
80
|
+
method="PATCH",
|
|
81
|
+
path="/rest/v1/<table>?name=eq.__openhack_test_probe",
|
|
82
|
+
headers={{"Prefer": "return=representation"}},
|
|
83
|
+
body={{"name": "__openhack_test_probe_updated"}}
|
|
84
|
+
)
|
|
85
|
+
```
|
|
86
|
+
- If response body shows updated row → UPDATE confirmed
|
|
87
|
+
|
|
88
|
+
4. **DELETE the canary (cleanup):**
|
|
89
|
+
```
|
|
90
|
+
supabase_http_request(
|
|
91
|
+
method="DELETE",
|
|
92
|
+
path="/rest/v1/<table>?or=(name.eq.__openhack_test_probe,name.eq.__openhack_test_probe_updated)",
|
|
93
|
+
headers={{"Prefer": "return=representation"}}
|
|
94
|
+
)
|
|
95
|
+
```
|
|
96
|
+
- If response body contains deleted row → DELETE confirmed, cleanup done
|
|
97
|
+
|
|
98
|
+
If any step fails (empty response, 403, no rows affected), the corresponding operation is NOT confirmed.
|
|
99
|
+
|
|
100
|
+
## Validation Approach
|
|
101
|
+
|
|
102
|
+
### For Runtime/Black-Box Findings (Supabase API-based):
|
|
103
|
+
|
|
104
|
+
1. **Data exposure claims:** Re-query the table with `supabase_http_request` or `supabase_query_table`. If actual rows with sensitive columns come back → confirmed. If 0 rows → false positive (RLS active).
|
|
105
|
+
|
|
106
|
+
2. **Write access claims:** Run the canary protocol above. Only confirmed if you can INSERT a row and SELECT it back.
|
|
107
|
+
|
|
108
|
+
3. **RPC abuse claims:** Re-call the function with `supabase_call_rpc` or `supabase_http_request`. If it returns actual sensitive data → confirmed. If error or empty → false positive.
|
|
109
|
+
|
|
110
|
+
4. **Storage claims:** Re-probe with `supabase_probe_storage`. If actual files are listed or downloaded → confirmed.
|
|
111
|
+
|
|
112
|
+
5. **Filter bypass / IDOR claims:** Reproduce the exact query. If data from other users/tenants appears → confirmed.
|
|
113
|
+
|
|
114
|
+
Write **practical PoCs** as Python scripts using `requests` so developers can run and inspect them clearly.
|
|
115
|
+
|
|
116
|
+
## CRITICAL: Python-First PoC Format (MUST FOLLOW)
|
|
117
|
+
|
|
118
|
+
For EACH confirmed finding, `poc` must be Python-first and include all exploit requirements in one place.
|
|
119
|
+
|
|
120
|
+
The `poc` field MUST contain, in this exact order:
|
|
121
|
+
1. `# Requirements` comment block:
|
|
122
|
+
- `Auth required: yes/no`
|
|
123
|
+
- `Token required: yes/no`
|
|
124
|
+
- `Token type/source: ...`
|
|
125
|
+
- `Prerequisites: ...`
|
|
126
|
+
2. Optional install line: `# Install: pip install requests`
|
|
127
|
+
3. Executable Python script using `requests` with:
|
|
128
|
+
- Full URL/path
|
|
129
|
+
- Full `headers` dict containing **every required header**
|
|
130
|
+
- Full payload/query params
|
|
131
|
+
- Explicit `Authorization` header format if auth is required
|
|
132
|
+
4. Optional expected response comment.
|
|
133
|
+
|
|
134
|
+
Do NOT return shell-only/curl-only PoCs unless explicitly requested by the user.
|
|
135
|
+
|
|
136
|
+
### For Static Analysis Findings (code-based):
|
|
137
|
+
|
|
138
|
+
These findings come from source code analysis and require RIGOROUS practical exploitability assessment. Code pattern matches alone are NOT sufficient to confirm -- you must prove the attack is actually feasible.
|
|
139
|
+
|
|
140
|
+
**Step 1: Read the full context**
|
|
141
|
+
- Read the full file context (not just the flagged line)
|
|
142
|
+
- Check if there are sanitization/validation steps we missed
|
|
143
|
+
- Trace the data flow completely from source to sink
|
|
144
|
+
- Look for security controls that might prevent exploitation (CSP headers, middleware, etc.)
|
|
145
|
+
|
|
146
|
+
**Step 2: Practical Exploitability Assessment (MANDATORY for static findings)**
|
|
147
|
+
|
|
148
|
+
You MUST evaluate these five criteria. If ANY criterion disqualifies the finding, mark it as `false_positive`.
|
|
149
|
+
|
|
150
|
+
**A. Attacker Prerequisites -- Are they realistic?**
|
|
151
|
+
- If the attacker must guess a UUID/CUID (122+ bits of entropy) to exploit the finding, mark it as FALSE POSITIVE. Brute-forcing UUIDs is computationally infeasible (~5.3 × 10^36 possibilities).
|
|
152
|
+
- If the attacker needs admin-level or highly privileged access just to reach the vulnerable code path, the severity is LOW at most.
|
|
153
|
+
- Read the PoC's own "Requirements" section critically. If it says "requires valid API key" or "requires knowing another user's ID" without explaining how the attacker gets these, the finding is impractical.
|
|
154
|
+
- **CRITICAL: Chained-vulnerability prerequisite rule.** If the PoC requires a *separate, pre-existing vulnerability* to work (e.g. "set a cookie via XSS", "inject via subdomain takeover", "requires MITM"), the finding is NOT independently exploitable. Either mark it as FALSE POSITIVE or downgrade severity to LOW. The prerequisite vulnerability is the real finding, not the secondary effect. An Open Redirect that requires XSS to set a cookie is at most LOW -- if you already have XSS, the redirect is irrelevant.
|
|
155
|
+
|
|
156
|
+
**B. Browser & Protocol Constraints -- Does the platform actually permit this?**
|
|
157
|
+
- CORS: `Access-Control-Allow-Origin: *` with `Access-Control-Allow-Credentials: true` is REJECTED by all browsers per the Fetch specification. If the finding relies on this combination, mark it as FALSE POSITIVE.
|
|
158
|
+
- CSRF: If the target uses `SameSite=Lax` cookies (browser default) or requires custom headers/tokens that cross-site requests cannot provide (Turnstile tokens, API keys in headers), mark CSRF as FALSE POSITIVE.
|
|
159
|
+
- SameSite=None: If intentionally set for embed/widget functionality (a product requirement), this is BY DESIGN, not a vulnerability.
|
|
160
|
+
|
|
161
|
+
**C. Existing Mitigations -- What defenses are in place?**
|
|
162
|
+
- Does the endpoint have rate limiting or bot protection (Turnstile, reCAPTCHA)?
|
|
163
|
+
- Are there downstream authorization checks (e.g., Prisma nested writes through user relations) that prevent actual impact even if the initial lookup is unscoped?
|
|
164
|
+
- Is there middleware, WAF, or infrastructure-level protection (HSTS at CDN, CSP via headers middleware)?
|
|
165
|
+
|
|
166
|
+
**D. Damage Assessment -- Is the impact meaningful?**
|
|
167
|
+
- "Attacker can trigger a password reset email" = NOT damage. The link goes to the legitimate user's email inbox.
|
|
168
|
+
- "Attacker can create a booking" on a scheduling app = NOT damage. Public booking is the product's core function.
|
|
169
|
+
- "Attacker can determine a UUID exists" = NOT damage. No sensitive information is revealed.
|
|
170
|
+
- "Attacker can view source code" of an open-source project = NOT damage. The code is already public on GitHub.
|
|
171
|
+
- "Missing security headers" without a companion injection vulnerability = NOT exploitable damage.
|
|
172
|
+
|
|
173
|
+
**E. Design Intent -- Is this behavior intentional? ("Too good to be true" check)**
|
|
174
|
+
|
|
175
|
+
This is the most important criterion. Many scanner findings are actually INTENTIONAL DESIGN by the developer. Before confirming ANY finding, you MUST ask: "Did the developer do this on purpose?"
|
|
176
|
+
|
|
177
|
+
**Signals of intentional design (finding is likely FALSE POSITIVE):**
|
|
178
|
+
- The code has a `@since` version tag (versioned, reviewed API — not accidental)
|
|
179
|
+
- Comments say "intentionally public", "by design", "no auth required", "allow anonymous"
|
|
180
|
+
- The endpoint is marked `publicProcedure`, `AUTHENTICATE = false`, `@csrf_exempt` with a clear comment explaining why
|
|
181
|
+
- The value is a dev-mode fallback: `process.env.SECRET || "default"`, `ENV.fetch("KEY", "fallback")`, with a production guard like `if (NODE_ENV !== 'production')`
|
|
182
|
+
- The "hardcoded secret" is actually a default that only applies in development (e.g., `jwtSecret: process.env.JWT_SECRET || "supersecret"`)
|
|
183
|
+
- The feature is documented in the project's README/docs as intentionally public
|
|
184
|
+
- The pattern is standard for the framework (e.g., Next.js `export const dynamic`, Rails `skip_before_action` for specific reasons)
|
|
185
|
+
|
|
186
|
+
**The "too good to be true" rule:** If a vulnerability in a popular, well-maintained project seems trivially exploitable and hasn't been reported before, it's almost certainly intentional design. A project with 10,000+ GitHub stars has been reviewed by thousands of developers. Ask yourself: "Is it more likely that thousands of developers missed this, or that it's by design?"
|
|
187
|
+
|
|
188
|
+
- Is the endpoint marked as `publicProcedure`, or documented as intentionally public?
|
|
189
|
+
- Is the configuration required for product functionality (embeds, widgets, public APIs, third-party integrations)?
|
|
190
|
+
- Would "fixing" the finding break core product functionality?
|
|
191
|
+
|
|
192
|
+
**Decision Matrix:**
|
|
193
|
+
- Fails ANY of A-E → `false_positive` (with explanation of which criterion failed)
|
|
194
|
+
- Passes all A-E with clear evidence → `confirmed`
|
|
195
|
+
- Uncertain on one criterion → `needs_more_info`
|
|
196
|
+
|
|
197
|
+
## Potential Vulnerabilities to Validate
|
|
198
|
+
|
|
199
|
+
{findings}
|
|
200
|
+
|
|
201
|
+
## Tools Available
|
|
202
|
+
|
|
203
|
+
### Runtime Probing Tools (use for re-verification and canary testing):
|
|
204
|
+
- `supabase_http_request` - **Raw HTTP request (curl equivalent).** Use for canary protocol, exact exploit reproduction, and any custom request. This is your primary validation tool.
|
|
205
|
+
- `supabase_query_table` - Re-query tables to verify access and data exposure
|
|
206
|
+
- `supabase_mutate_table` - Re-test write operations (now returns full response body + affected_count)
|
|
207
|
+
- `supabase_call_rpc` - Re-call RPC functions to verify responses
|
|
208
|
+
- `supabase_probe_storage` - Re-probe storage buckets
|
|
209
|
+
- `supabase_graphql_query` - Re-run GraphQL queries
|
|
210
|
+
|
|
211
|
+
### Static Analysis Tools (when source code available):
|
|
212
|
+
- `read_file` - Read full file context
|
|
213
|
+
- `trace_variable` - Follow data flow
|
|
214
|
+
- `extract_imports` - Check what's imported
|
|
215
|
+
- `grep` - Search for related code
|
|
216
|
+
|
|
217
|
+
## Validation Process
|
|
218
|
+
|
|
219
|
+
For each finding:
|
|
220
|
+
1. **Think** - What needs to be proven? What would make this a false positive?
|
|
221
|
+
2. **Exploit** - Use `supabase_http_request` or other tools to actually exploit it
|
|
222
|
+
3. **Check evidence** - Did the response contain actual data/rows/modifications?
|
|
223
|
+
4. **Canary test** (for write claims) - Run the INSERT→SELECT→UPDATE→DELETE cycle
|
|
224
|
+
5. **Think** - Is this a confirmed vulnerability or a false positive? What's the real severity?
|
|
225
|
+
6. **Craft PoC** - Write a practical, copy-pasteable Python exploit script
|
|
226
|
+
7. **Suggest fix** - Provide a concrete remediation (SQL for RLS, code changes, etc.)
|
|
227
|
+
|
|
228
|
+
## CRITICAL: How to Report Results (MUST USE TOOLS)
|
|
229
|
+
|
|
230
|
+
You MUST use tools to report validation results. Do NOT write text summaries - always use tool calls.
|
|
231
|
+
|
|
232
|
+
### Step 1: For EACH finding, call `validate_finding`
|
|
233
|
+
|
|
234
|
+
Call `validate_finding` for EVERY finding (1, 2, 3, etc.) with these parameters:
|
|
235
|
+
- `finding_index`: The finding number (1-based)
|
|
236
|
+
- `status`: "confirmed", "false_positive", or "needs_more_info"
|
|
237
|
+
- `confidence`: "high" (proven with actual data/canary), "medium" (partially proven), "low" (uncertain)
|
|
238
|
+
- `cvss_score`: CVSS 3.1 score (0.0-10.0) for confirmed findings
|
|
239
|
+
- `evidence`: ACTUAL response data that proves the exploit. For data leaks: include row samples. For write access: include canary proof. For false positives: explain what the actual response was (e.g., "0 rows returned, RLS active").
|
|
240
|
+
- `poc`: Proof of concept -- Python (`requests`) code that demonstrates the exploit. Must be copy-pasteable and include a `# Requirements` block. Include the actual Supabase URL. CRITICAL: NEVER include actual publishable/anon keys -- ALWAYS use the placeholder `$SUPABASE_PUBLISHABLE_KEY$` for any apikey or Authorization Bearer value. The UI will substitute the real key when copied.
|
|
241
|
+
- `fix`: Recommended remediation. For Supabase: include the SQL to add RLS policies, fix RPC functions, etc.
|
|
242
|
+
|
|
243
|
+
### Step 2: After ALL findings validated, call `finish_validation`
|
|
244
|
+
|
|
245
|
+
When you have called `validate_finding` for ALL findings, call `finish_validation` to complete:
|
|
246
|
+
- `summary`: Brief summary of results
|
|
247
|
+
- `total_confirmed`: Number of confirmed vulnerabilities
|
|
248
|
+
- `total_false_positives`: Number of false positives
|
|
249
|
+
|
|
250
|
+
### Example Workflow
|
|
251
|
+
|
|
252
|
+
```
|
|
253
|
+
# CONFIRMED: actual data leak proven with response data
|
|
254
|
+
validate_finding(
|
|
255
|
+
finding_index=1,
|
|
256
|
+
status="confirmed",
|
|
257
|
+
confidence="high",
|
|
258
|
+
cvss_score=9.1,
|
|
259
|
+
evidence="Independently verified: GET /rest/v1/secrets?select=* returned 3 rows: [{{'id': 1, 'service_name': 'stripe', 'api_key': 'sk_live_...'}}]. Real API keys exposed to anonymous users.",
|
|
260
|
+
poc="# Requirements\n# - Auth required: no\n# - Token required: yes\n# - Token type/source: Supabase publishable key\n# - Prerequisites: Project Supabase URL and publishable key\n# Install: pip install requests\nimport requests\n\nurl = \"https://abc.supabase.co/rest/v1/secrets?select=*\"\nheaders = {{\n \"apikey\": \"$SUPABASE_PUBLISHABLE_KEY$\",\n \"Authorization\": \"Bearer $SUPABASE_PUBLISHABLE_KEY$\",\n}}\n\nresponse = requests.get(url, headers=headers, timeout=30)\nprint(response.status_code)\nprint(response.text)",
|
|
261
|
+
fix="ALTER TABLE secrets ENABLE ROW LEVEL SECURITY;\nCREATE POLICY secrets_select ON secrets FOR SELECT USING (auth.uid() = owner_id);"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
# CONFIRMED: write access proven via canary
|
|
265
|
+
validate_finding(
|
|
266
|
+
finding_index=2,
|
|
267
|
+
status="confirmed",
|
|
268
|
+
confidence="high",
|
|
269
|
+
cvss_score=7.5,
|
|
270
|
+
evidence="Canary test completed: 1) INSERT __openhack_test_probe → 201, response body: {{'id': 42, 'name': '__openhack_test_probe'}}. 2) SELECT it back → 1 row returned. 3) UPDATE → row changed to __openhack_test_probe_updated. 4) DELETE → cleanup successful. Full CRUD access confirmed for anonymous users.",
|
|
271
|
+
poc="# Requirements\n# - Auth required: no\n# - Token required: yes\n# - Token type/source: Supabase publishable key\n# - Prerequisites: Table allows anon writes\n# Install: pip install requests\nimport requests\n\nurl = \"https://abc.supabase.co/rest/v1/public_notes\"\nheaders = {{\n \"apikey\": \"$SUPABASE_PUBLISHABLE_KEY$\",\n \"Authorization\": \"Bearer $SUPABASE_PUBLISHABLE_KEY$\",\n \"Content-Type\": \"application/json\",\n \"Prefer\": \"return=representation\",\n}}\npayload = {{\"name\": \"__openhack_test_probe\"}}\n\nresponse = requests.post(url, headers=headers, json=payload, timeout=30)\nprint(response.status_code)\nprint(response.text)",
|
|
272
|
+
fix="ALTER TABLE public_notes ENABLE ROW LEVEL SECURITY;\nCREATE POLICY public_notes_insert ON public_notes FOR INSERT WITH CHECK (auth.uid() IS NOT NULL);\nCREATE POLICY public_notes_select ON public_notes FOR SELECT USING (auth.uid() = user_id);"
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
# FALSE POSITIVE: no actual data exposed (runtime)
|
|
276
|
+
validate_finding(
|
|
277
|
+
finding_index=3,
|
|
278
|
+
status="false_positive",
|
|
279
|
+
confidence="high",
|
|
280
|
+
evidence="Re-queried GET /rest/v1/users?select=* -- returned 200 with 0 rows. RLS is filtering correctly. The endpoint is accessible but no data is leaked."
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
# FALSE POSITIVE: static finding fails exploitability assessment
|
|
284
|
+
validate_finding(
|
|
285
|
+
finding_index=4,
|
|
286
|
+
status="false_positive",
|
|
287
|
+
confidence="high",
|
|
288
|
+
evidence="IDOR in apiKeys delete handler: the code does findUnique({{where: {{id}}}}) without userId filter, but API key IDs are UUIDs (122 bits of entropy). Brute-forcing UUIDs is computationally infeasible. Additionally, the actual delete operation goes through the user relation (prisma nested write) which prevents cross-user deletion. Fails criterion A (unrealistic prerequisites) and C (downstream mitigation exists)."
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
# FALSE POSITIVE: CORS misconfiguration that browsers block
|
|
292
|
+
validate_finding(
|
|
293
|
+
finding_index=5,
|
|
294
|
+
status="false_positive",
|
|
295
|
+
confidence="high",
|
|
296
|
+
evidence="CORS finding claims Access-Control-Allow-Origin: * with credentials: true enables cross-origin attacks. However, per the Fetch specification, browsers REJECT responses with Access-Control-Allow-Origin: * when credentials mode is 'include'. This combination is self-defeating and not exploitable. Additionally, the v1 API uses API keys (not session cookies) for authentication, so cookie-riding attacks are not applicable. Fails criterion B (browser rejects this) and A (attacker needs API key anyway)."
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
# Then signal completion
|
|
300
|
+
finish_validation(summary="Validated 5 findings: 2 confirmed (1 critical data leak, 1 high write access), 3 false positives (1 RLS active, 1 UUID-based IDOR, 1 browser-blocked CORS)", total_confirmed=2, total_false_positives=3)
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
IMPORTANT: Do NOT stop until you have:
|
|
304
|
+
1. Called `validate_finding` for ALL findings (including ones you determine are false positives)
|
|
305
|
+
2. Called `finish_validation` to complete
|
|
306
|
+
|
|
307
|
+
Be RUTHLESS about false positives. False positives waste developer time and erode trust in the scanner.
|
|
308
|
+
|
|
309
|
+
**For runtime findings:** A 200 status code with 0 rows is NOT a vulnerability. A 204 on a dummy UUID is NOT a confirmed write. Only ACTUAL DATA in the response proves an exploit.
|
|
310
|
+
|
|
311
|
+
**For static findings:** A code pattern match is NOT a confirmed vulnerability. You MUST evaluate practical exploitability using criteria A-E above. Key automatic disqualifiers:
|
|
312
|
+
- IDOR where IDs are UUIDs/CUIDs → FALSE POSITIVE (cannot guess 122-bit random IDs)
|
|
313
|
+
- CORS `*` + credentials: true → FALSE POSITIVE (browsers reject this per Fetch spec)
|
|
314
|
+
- CSRF on forgot-password/signup/booking endpoints → FALSE POSITIVE (by-design public, no meaningful damage)
|
|
315
|
+
- Missing headers without companion injection → FALSE POSITIVE (informational only)
|
|
316
|
+
- Source maps on open-source projects → FALSE POSITIVE (code already public)
|
|
317
|
+
- SameSite=None for embed functionality → FALSE POSITIVE (intentional product requirement)
|
|
318
|
+
- **Non-production code** → FALSE POSITIVE. Before confirming ANY finding, check the file path. If the vulnerable code is in a demo, example, sample, test, tutorial, playground, CLI tool, debug utility, or benchmark — it is NOT a production vulnerability. Use your judgment: does this code actually run in deployed systems? A timing side-channel in a CLI tool nobody deploys is not a CVE. A timing side-channel in the server's authentication handler is.
|
|
319
|
+
"""
|