From 05100f7f1cdec52d7da24c7620f290843bdb7404 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 12:24:10 +0900 Subject: [PATCH 1/3] test(examples): require semantic project identifiers --- tests/test_example_schema_naming_contract.py | 45 ++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 tests/test_example_schema_naming_contract.py diff --git a/tests/test_example_schema_naming_contract.py b/tests/test_example_schema_naming_contract.py new file mode 100644 index 00000000..9d4d180f --- /dev/null +++ b/tests/test_example_schema_naming_contract.py @@ -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 From 6ea24c8f275f231c1b2f703786efa970960aafba Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 12:24:49 +0900 Subject: [PATCH 2/3] docs(examples): semanticize vulnerable project identifiers --- examples/vulnerable-vibe-app/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/vulnerable-vibe-app/README.md b/examples/vulnerable-vibe-app/README.md index 0a0164f3..ff8f0088 100644 --- a/examples/vulnerable-vibe-app/README.md +++ b/examples/vulnerable-vibe-app/README.md @@ -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!, @@ -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); } ``` @@ -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; From 84176a1abcfc99931fa23dccea57a20a877953ea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 12:25:26 +0900 Subject: [PATCH 3/3] docs(examples): semanticize fixed project identifiers --- examples/fixed-vibe-app/README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/examples/fixed-vibe-app/README.md b/examples/fixed-vibe-app/README.md index 5588d2c9..a90f3d44 100644 --- a/examples/fixed-vibe-app/README.md +++ b/examples/fixed-vibe-app/README.md @@ -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 @@ -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(); @@ -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); } ``` @@ -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 @@ -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);