Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions examples/fixed-vibe-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ See `../vulnerable-vibe-app/README.md` for the list of vulnerabilities that were

### 1. Ownership Check Added (IDOR Fixed)

`app/api/projects/[id]/route.ts`
`app/api/projects/[projectId]/route.ts`

```typescript
// ✅ SECURE: Authentication + ownership verification
Expand All @@ -19,7 +19,7 @@ import { createClient } from '@/lib/supabase/server';

export async function GET(
req: Request,
{ params }: { params: { id: string } }
{ params }: { params: { projectId: string } }
) {
// Step 1: Check authentication
const session = await auth();
Expand All @@ -30,18 +30,18 @@ export async function GET(
const supabase = createClient();

// Step 2: Fetch the project
const { data: project, error } = await supabase
const { data: projectRecord, error: projectQueryError } = await supabase
.from('projects')
.select('*')
.eq('id', params.id)
.eq('project_id', params.projectId)
.single();

// Step 3: Verify ownership
if (error || !project || project.user_id !== session.user.id) {
if (projectQueryError || !projectRecord || projectRecord.user_id !== session.user.id) {
return Response.json({ error: 'Forbidden' }, { status: 403 });
}

return Response.json(project);
return Response.json(projectRecord);
}
```

Expand Down Expand Up @@ -196,10 +196,10 @@ const db = new PrismaClient(); // uses DATABASE_URL from process.env automatical
```sql
-- ✅ SECURE: RLS enabled with proper policies
CREATE TABLE projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id) NOT NULL,
name TEXT NOT NULL,
data JSONB
project_name TEXT NOT NULL,
project_payload_json JSONB
);

-- Enable Row Level Security
Expand Down Expand Up @@ -271,7 +271,7 @@ export async function GET(req: Request) {
Each fixed endpoint has corresponding tests:

```typescript
describe('GET /api/projects/[id]', () => {
describe('GET /api/projects/[projectId]', () => {
it('returns 401 when unauthenticated', async () => {
const res = await GET(request('/api/projects/test-id'));
expect(res.status).toBe(401);
Expand Down
16 changes: 8 additions & 8 deletions examples/vulnerable-vibe-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ See `../fixed-vibe-app/` for the corrected version.

### 1. Missing Ownership Check (IDOR)

`app/api/projects/[id]/route.ts`
`app/api/projects/[projectId]/route.ts`

```typescript
// ❌ VULNERABLE: No ownership check — any user can access any project
export async function GET(
req: Request,
{ params }: { params: { id: string } }
{ params }: { params: { projectId: string } }
) {
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
Expand All @@ -27,13 +27,13 @@ export async function GET(

// No authentication check!
// No ownership verification!
const { data, error } = await supabase
const { data: projectRecord, error: projectQueryError } = await supabase
.from('projects')
.select('*')
.eq('id', params.id)
.eq('project_id', params.projectId)
.single();

return Response.json(data);
return Response.json(projectRecord);
}
```

Expand Down Expand Up @@ -135,10 +135,10 @@ const db = new PrismaClient({
```sql
-- ❌ VULNERABLE: RLS never enabled, no policies
CREATE TABLE projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id),
name TEXT,
data JSONB
project_name TEXT,
project_payload_json JSONB
);

-- Missing: ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
Expand Down
45 changes: 45 additions & 0 deletions tests/test_example_schema_naming_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Guard semantic naming in the paired project security examples."""

from pathlib import Path

import pytest


REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
PROJECT_EXAMPLE_READMES = (
Path("examples/vulnerable-vibe-app/README.md"),
Path("examples/fixed-vibe-app/README.md"),
)


@pytest.mark.parametrize("example_readme_path", PROJECT_EXAMPLE_READMES)
def test_project_example_uses_semantic_owned_identifiers(
example_readme_path: Path,
) -> None:
"""Keep project-owned route and database identifiers context-specific."""
example_text = (REPOSITORY_ROOT / example_readme_path).read_text(encoding="utf-8")

assert "app/api/projects/[projectId]/route.ts" in example_text
assert "params: { projectId: string }" in example_text
assert ".eq('project_id', params.projectId)" in example_text
assert "project_id UUID PRIMARY KEY DEFAULT gen_random_uuid()" in example_text
assert "project_name TEXT" in example_text
assert "project_payload_json JSONB" in example_text

assert "app/api/projects/[id]/route.ts" not in example_text
assert "params: { id: string }" not in example_text
assert ".eq('id', params.id)" not in example_text
assert "\n id UUID PRIMARY KEY DEFAULT gen_random_uuid()," not in example_text
assert "\n name TEXT" not in example_text
assert "\n data JSONB" not in example_text

# Supabase's auth schema owns this external identifier; do not rewrite it.
assert "REFERENCES auth.users(id)" in example_text


def test_supabase_result_fields_are_aliased_at_the_example_boundary() -> None:
"""Translate vendor result keys into semantic internal names in both examples."""
for example_readme_path in PROJECT_EXAMPLE_READMES:
example_text = (REPOSITORY_ROOT / example_readme_path).read_text(encoding="utf-8")
assert "data: projectRecord" in example_text
assert "error: projectQueryError" in example_text
Loading