Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
# Add User ID to Team Sessions Table

1. Changes
- Add `user_id` (uuid) column to track session ownership
- Add foreign key to profiles.user_id
- Add index on user_id for query performance

2. Security
- Enables RLS policies to validate ownership via auth.uid()
- Allows session management to be tied to user authentication
*/

ALTER TABLE team_sessions
ADD COLUMN user_id uuid REFERENCES profiles(user_id) ON DELETE CASCADE;

CREATE INDEX IF NOT EXISTS idx_team_sessions_user_id ON team_sessions(user_id);
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
# Secure RLS Policies for Team Sessions Table

1. Changes
- Drop insecure public RLS policies
- Add authenticated-only RLS policies with auth.uid() checks
- Prevent users from viewing/modifying other teams' sessions
- Remove public delete policy entirely
- Add service role bypass for admin operations

2. Security
- Users can only view/update their own team's sessions
- Session management is tied to user authentication
- No public access allowed
- Delete operations require service role
*/

-- Drop insecure public policies
DROP POLICY IF EXISTS "Allow public read team sessions" ON team_sessions;
DROP POLICY IF EXISTS "Allow public insert team sessions" ON team_sessions;
DROP POLICY IF EXISTS "Allow public update team sessions" ON team_sessions;
DROP POLICY IF EXISTS "Allow public delete team sessions" ON team_sessions;

-- Allow authenticated users to read only their own team's sessions
CREATE POLICY "Users can read own team sessions"
ON team_sessions
FOR SELECT
USING (user_id = auth.uid());

-- Allow authenticated users to insert sessions for their own team
CREATE POLICY "Users can insert own team sessions"
ON team_sessions
FOR INSERT
WITH CHECK (user_id = auth.uid());

-- Allow authenticated users to update only their own team's sessions
CREATE POLICY "Users can update own team sessions"
ON team_sessions
FOR UPDATE
USING (user_id = auth.uid())
WITH CHECK (user_id = auth.uid());

-- Note: DELETE policy intentionally omitted - use service role for admin operations
-- This prevents users from accidentally or maliciously deleting sessions
13 changes: 11 additions & 2 deletions Docs/ADMIN_SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,21 @@ localStorage keys per team:
## Database Tables

### team_sessions table
Tracks active login sessions:
Tracks active login sessions with user authentication:
- `id` (uuid, primary key)
- `user_id` (uuid, foreign key) - References profiles.user_id
- `team_id` (text, UNIQUE) - Team identifier
- `device_id` (text) - Device login identifier
- `logged_in_at` (timestamp) - Login time
- `last_activity` (timestamp) - Last activity time
- `is_active` (boolean) - Current session status

**Security:**
- RLS policies enforce user authentication (auth.uid() checks)
- Users can only view/modify their own team's sessions
- Delete operations restricted to service role only (admin operations)
- No public access allowed - all operations require authentication

### leaderboard table
Challenge completion records:
- `id` (uuid, primary key)
Expand All @@ -65,7 +72,9 @@ Challenge completion records:
- `src/components/AuthPage.tsx` - Team login
- `src/components/ChallengePage.tsx` - Multi-question challenge flow
- `src/data/teamData.ts` - Pre-registered teams
- `supabase/migrations/20251102_create_team_sessions.sql` - Session table
- `supabase/migrations/20251104204240_20251102_create_team_sessions.sql` - Session table (initial)
- `supabase/migrations/20260201_add_user_id_to_team_sessions.sql` - Add user tracking
- `supabase/migrations/20260202_secure_team_sessions_rls.sql` - Secure RLS policies
- `supabase/migrations/20251102_create_leaderboard.sql` - Leaderboard table

## Sample Questions
Expand Down
177 changes: 177 additions & 0 deletions Docs/SECURITY_FIX_TEAM_SESSIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
# Team Sessions Security Fix - Issue #71

## Vulnerability Description

The initial RLS policies for the `team_sessions` table had critical security issues:

### Problems Identified:
1. **Public Read Access**: Anyone could view all active sessions using `USING (true)` policy
2. **Public Write Access**: Anyone could insert/update sessions without authentication
3. **Public Delete Access**: Anyone could delete any team's session record
4. **No Ownership Tracking**: No connection between sessions and user authentication

### Attack Scenarios:
- **Session Hijacking**: Any user could modify `device_id` to claim a team session
- **Forced Logout**: Any user could mark another team's session as `is_active = false`
- **Data Exfiltration**: Session data was readable by unauthenticated users
- **Denial of Service**: Malicious users could delete session records

## Solution Implemented

### 1. Added User Ownership Tracking
**Migration**: `20260201_add_user_id_to_team_sessions.sql`

```sql
ALTER TABLE team_sessions
ADD COLUMN user_id uuid REFERENCES profiles(user_id) ON DELETE CASCADE;
```

- Links sessions to authenticated users via `profiles.user_id`
- Cascading delete ensures data integrity
- Index on `user_id` for query performance

### 2. Secured RLS Policies
**Migration**: `20260202_secure_team_sessions_rls.sql`

#### Removed Insecure Policies:
- ❌ `Allow public read team sessions`
- ❌ `Allow public insert team sessions`
- ❌ `Allow public update team sessions`
- ❌ `Allow public delete team sessions`

#### Added Authenticated Policies:
- ✅ `Users can read own team sessions` - Only see your own sessions
- ✅ `Users can insert own team sessions` - Create sessions only for yourself
- ✅ `Users can update own team sessions` - Modify only your own sessions
- ✅ **No delete policy** - Prevents accidental/malicious deletions

### 3. Service Role Operations

Delete operations must use the service role (admin-only):

```typescript
// This will fail (no delete policy for authenticated users)
await supabase
.from('team_sessions')
.delete()
.eq('id', sessionId);

// This works (service role bypass)
const { createClient } = require('@supabase/supabase-js');
const supabaseAdmin = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SERVICE_ROLE_KEY
);

await supabaseAdmin
.from('team_sessions')
.delete()
.eq('id', sessionId);
```

## Usage Examples

### Create a Session (Authenticated User)
```typescript
const { data, error } = await supabase
.from('team_sessions')
.insert({
user_id: (await supabase.auth.getUser()).data.user?.id,
team_id: 'team_123',
device_id: 'device_abc',
is_active: true
});

// ✅ Works - user_id matches authenticated user
// ❌ Fails - if user_id doesn't match authenticated user ID
```

### Read Session (Authenticated User)
```typescript
const { data, error } = await supabase
.from('team_sessions')
.select()
.eq('team_id', 'team_123');

// ✅ Returns session only if user_id = auth.uid()
// ❌ No data returned if user doesn't own this session
```

### Update Session (Authenticated User)
```typescript
const { error } = await supabase
.from('team_sessions')
.update({ is_active: false })
.eq('id', sessionId);

// ✅ Works - user owns this session
// ❌ Fails - user doesn't own this session
```

### Delete Session (Admin Only)
```typescript
// Frontend - Use Edge Function with service role verification
const { data, error } = await supabase.functions.invoke('admin-logout-team', {
body: { session_id: sessionId }
});

// Edge Function - Uses service role
const supabaseAdmin = createClient(
Deno.env.get('SUPABASE_URL'),
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')
);

await supabaseAdmin
.from('team_sessions')
.delete()
.eq('id', sessionId);
```

## Migration Steps

1. **Run Migration 1**: Add `user_id` column
- Populates `user_id` for existing sessions (manual data migration if needed)

2. **Run Migration 2**: Replace RLS policies
- Old policies automatically dropped
- New authenticated policies created

3. **Update Application Code**:
- Pass `user_id` when creating sessions
- Verify operations work with new RLS constraints

4. **Test Thoroughly**:
- Verify users can only access their own sessions
- Confirm cross-team access is blocked
- Validate delete operations (should fail for regular users)

## Security Checklist

- ✅ RLS policies use `auth.uid()` checks
- ✅ No public access policies
- ✅ User ownership tracked via `user_id` column
- ✅ Foreign key relationship to `profiles.user_id`
- ✅ Delete policy removed (service role only)
- ✅ Cascading delete on user profile deletion
- ✅ Indexes on frequently queried columns

## Compliance

This fix addresses:
- **CWE-639**: Authorization Bypass Through User-Controlled Key
- **OWASP A01:2021** - Broken Access Control
- **OWASP A05:2021** - Access Control

## Future Improvements

1. **Audit Logging**: Log all session modifications (who, what, when)
2. **Rate Limiting**: Prevent brute-force session creation
3. **Session Timeout**: Auto-invalidate sessions after inactivity period
4. **Device Fingerprinting**: Enhanced device ID tracking (user-agent, IP, etc.)
5. **Multi-Factor Authentication**: Require MFA for high-risk operations

## References

- [Supabase Row Level Security](https://supabase.com/docs/guides/auth/row-level-security)
- [PostgreSQL ALTER TABLE](https://www.postgresql.org/docs/current/sql-altertable.html)
- [OWASP Access Control](https://owasp.org/Top10/A01_2021-Broken_Access_Control/)
72 changes: 46 additions & 26 deletions src/components/ChallengePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -376,39 +376,59 @@ export function ChallengePage({ teamId, teamName, leaderName, onLogout }: Challe
};

const revealNextHint = async () => {
if (!question || !challenge) return;
if (!question || !challenge || !isSupabaseConfigured) return;

const nextHintIndex = revealedHints.length;
if (nextHintIndex >= question.hints.length) return;

const cost = 10; // Cost per hint
if (points < cost) return;
try {
// Call the reveal-hint Edge Function for atomic point deduction
const { data, error } = await supabase.functions.invoke('reveal-hint', {
body: {
team_name: teamName
}
});

const newPoints = points - cost;
const newRevealedHints = [...revealedHints, nextHintIndex];
const newHintsUsed = (challenge.hintsUsed || 0) + 1;
if (error) {
console.error('Reveal hint error:', error);
alert('Failed to reveal hint. Please try again.');
return;
}

setPoints(newPoints);
setRevealedHints(newRevealedHints);
// Check if the operation was successful
if (!data.success) {
if (data.reason === 'concurrent_modification') {
// Retry by reloading the challenge
await loadChallenge();
alert('Points were modified. Refreshed your points. Please try again.');
} else {
alert(data.error || 'Insufficient points to reveal hint.');
}
return;
}

const updatedChallenge = {
...challenge,
hintsUsed: newHintsUsed
};
setChallenge(updatedChallenge);

localStorage.setItem(`cybergauntlet_progress_${teamId}`, JSON.stringify({
...updatedChallenge,
elapsedTime,
revealedHints: newRevealedHints
}));

// Update points in database
if (isSupabaseConfigured) {
await supabase
.from('profiles')
.update({ points: newPoints })
.eq('team_name', teamName);
// Update local state with the new points from the server
const newRevealedHints = [...revealedHints, nextHintIndex];
const newHintsUsed = (challenge.hintsUsed || 0) + 1;

// Update UI with the server-confirmed points
setPoints(data.new_points);
setRevealedHints(newRevealedHints);

const updatedChallenge = {
...challenge,
hintsUsed: newHintsUsed
};
setChallenge(updatedChallenge);

localStorage.setItem(`cybergauntlet_progress_${teamId}`, JSON.stringify({
...updatedChallenge,
elapsedTime,
revealedHints: newRevealedHints
}));
} catch (err) {
console.error('Error revealing hint:', err);
alert('Failed to reveal hint. Please try again.');
}
};

Expand Down
Loading
Loading