GT AI OS Community Edition v2.0.33

Security hardening release addressing CodeQL and Dependabot alerts:

- Fix stack trace exposure in error responses
- Add SSRF protection with DNS resolution checking
- Implement proper URL hostname validation (replaces substring matching)
- Add centralized path sanitization to prevent path traversal
- Fix ReDoS vulnerability in email validation regex
- Improve HTML sanitization in validation utilities
- Fix capability wildcard matching in auth utilities
- Update glob dependency to address CVE
- Add CodeQL suppression comments for verified false positives

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
HackWeasel
2025-12-12 17:04:45 -05:00
commit b9dfb86260
746 changed files with 232071 additions and 0 deletions

View File

@@ -0,0 +1,192 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { useIdleTimer } from 'react-idle-timer';
import { useAuthStore, useHasHydrated } from '@/stores/auth-store';
import { refreshToken } from '@/services/auth';
import { SessionTimeoutModal } from '@/components/session/session-timeout-modal';
import { SESSION_CONFIG } from '@/config/session';
interface IdleTimerProviderProps {
children: React.ReactNode;
}
/**
* GT 2.0 Idle Timer Provider
*
* OWASP/NIST Compliant Session Management (Issues #242, #264)
*
* This provider implements a hybrid approach:
* - Server-side session tracking is AUTHORITATIVE (Issue #264)
* - Client-side IdleTimer provides UX enhancement and backup
*
* Server signals processed:
* - X-Session-Warning: <seconds> - Server says session is about to expire
* - X-Session-Expired: idle|absolute - Server says session has expired
*
* Client-side configuration (secondary to server):
* - Production: 30 minutes total, warning at 25 minutes (5 min before)
* - Multi-tab sync: enabled via crossTab
* - Only active when authenticated
*
* Uses react-idle-timer with promptBeforeIdle pattern
* @see https://idletimer.dev for documentation
* @see SESSION_CONFIG for timeout values
*/
export function IdleTimerProvider({ children }: IdleTimerProviderProps) {
const { isAuthenticated, logout, setToken } = useAuthStore();
const hasHydrated = useHasHydrated(); // Wait for Zustand to hydrate from localStorage
const [showModal, setShowModal] = useState(false);
const [remainingTime, setRemainingTime] = useState(0);
// Debug logging
useEffect(() => {
console.log('[IdleTimer] Provider mounted, isAuthenticated:', isAuthenticated, 'hasHydrated:', hasHydrated);
console.log('[IdleTimer] Config:', {
timeout: SESSION_CONFIG.TIMEOUT_MS,
promptBeforeIdle: SESSION_CONFIG.PROMPT_BEFORE_IDLE_MS,
});
}, [isAuthenticated, hasHydrated]);
// Listen for server-side session signals (Issue #264)
// The server is the authoritative source of truth for session state
useEffect(() => {
if (typeof window === 'undefined') return;
const handleServerWarning = (event: CustomEvent<{ secondsRemaining: number }>) => {
console.log('[IdleTimer] Server session warning received:', event.detail.secondsRemaining, 'seconds remaining');
// Show the warning modal if not already showing
if (!showModal) {
setShowModal(true);
setRemainingTime(event.detail.secondsRemaining);
}
};
const handleServerExpired = (event: CustomEvent<{ reason: string }>) => {
console.log('[IdleTimer] Server session expired:', event.detail.reason);
setShowModal(false);
// Map server expiry reason to logout reason
const logoutReason = event.detail.reason === 'absolute' ? 'session_expired' : 'expired';
logout(logoutReason);
};
window.addEventListener('session-warning', handleServerWarning as EventListener);
window.addEventListener('session-expired', handleServerExpired as EventListener);
return () => {
window.removeEventListener('session-warning', handleServerWarning as EventListener);
window.removeEventListener('session-expired', handleServerExpired as EventListener);
};
}, [showModal, logout]);
const handleOnPrompt = useCallback(() => {
console.log('[IdleTimer] Session expiring soon - showing warning modal');
setShowModal(true);
// Initial remaining time in seconds
setRemainingTime(Math.floor(SESSION_CONFIG.PROMPT_BEFORE_IDLE_MS / 1000));
}, []);
const handleOnIdle = useCallback(() => {
console.log('[IdleTimer] Session expired due to inactivity - logging out');
setShowModal(false);
logout('expired');
}, [logout]);
const handleOnActive = useCallback(() => {
// Note: onActive only fires when activate() is called while isPrompted
// Not when user moves mouse during countdown
console.log('[IdleTimer] Session reactivated');
setShowModal(false);
}, []);
const { activate, getRemainingTime, isPrompted } = useIdleTimer({
// Total inactivity timeout (see SESSION_CONFIG for values)
timeout: SESSION_CONFIG.TIMEOUT_MS,
// Show prompt before timeout (see SESSION_CONFIG for values)
promptBeforeIdle: SESSION_CONFIG.PROMPT_BEFORE_IDLE_MS,
// Event handlers
onPrompt: handleOnPrompt,
onIdle: handleOnIdle,
onActive: handleOnActive,
// Events to track (react-idle-timer defaults + focus)
events: SESSION_CONFIG.EVENTS as unknown as string[],
// Throttle event processing for performance
eventsThrottle: SESSION_CONFIG.EVENTS_THROTTLE_MS,
// Multi-tab synchronization via BroadcastChannel
crossTab: true,
syncTimers: 200,
// Only run when authenticated AND after Zustand has hydrated from localStorage
// Fix for Issue #264: Timer was disabled during hydration race condition
disabled: !hasHydrated || !isAuthenticated,
// Start automatically when mounted
startOnMount: true,
});
// Update countdown every second when modal is shown
useEffect(() => {
let interval: NodeJS.Timeout | null = null;
if (showModal && isPrompted()) {
interval = setInterval(() => {
const remaining = Math.ceil(getRemainingTime() / 1000);
setRemainingTime(remaining);
// Safety check - if remaining hits 0, ensure we log out
if (remaining <= 0) {
setShowModal(false);
logout('expired');
}
}, 1000);
}
return () => {
if (interval) {
clearInterval(interval);
}
};
}, [showModal, isPrompted, getRemainingTime, logout]);
const handleExtendSession = useCallback(async () => {
console.log('[IdleTimer] User extended session - refreshing token');
try {
// Refresh the JWT token (NIST/OWASP compliant - Issue #242)
const result = await refreshToken();
if (result.success) {
console.log('[IdleTimer] Token refreshed successfully');
// Reset the idle timer and hide modal
activate();
setShowModal(false);
} else if (result.error === 'absolute_timeout') {
// 8-hour absolute session limit reached (Issue #242)
// Show different message - user MUST re-login
console.log('[IdleTimer] Absolute session timeout (8 hours) - forcing re-login');
logout('session_expired');
} else {
console.error('[IdleTimer] Token refresh failed - logging out');
logout('expired');
}
} catch (error) {
console.error('[IdleTimer] Error refreshing token:', error);
logout('expired');
}
}, [activate, logout]);
const handleLogoutNow = useCallback(() => {
console.log('[IdleTimer] User clicked logout from warning modal');
setShowModal(false);
logout('manual');
}, [logout]);
return (
<>
{children}
<SessionTimeoutModal
open={showModal}
remainingTime={remainingTime}
onExtendSession={handleExtendSession}
onLogout={handleLogoutNow}
/>
</>
);
}

View File

@@ -0,0 +1,158 @@
'use client';
import { useState, useEffect, useCallback, useRef } from 'react';
import { useAuthStore, useHasHydrated } from '@/stores/auth-store';
import { getAuthToken } from '@/services/auth';
import { SessionTimeoutModal } from '@/components/session/session-timeout-modal';
interface SessionMonitorProps {
children: React.ReactNode;
}
// Polling interval: 60 seconds
const POLL_INTERVAL_MS = 60 * 1000;
/**
* GT 2.0 Session Monitor (NIST SP 800-63B AAL2 Compliant)
*
* Server-authoritative session management with two timeout types:
* - Idle timeout (30 min): Resets with activity - polling acts as heartbeat
* - Absolute timeout (12 hr): Cannot be extended - forces re-authentication
*
* How it works:
* 1. Polls /api/v1/auth/session/status every 60 seconds
* 2. Polling resets idle timeout, so active users won't hit idle limit
* 3. When absolute timeout < 30 min remaining, show informational notice
* 4. User acknowledges notice (can't extend absolute timeout)
* 5. If is_valid=false, force logout
*/
export function SessionMonitor({ children }: SessionMonitorProps) {
const { isAuthenticated, logout } = useAuthStore();
const hasHydrated = useHasHydrated();
const [showModal, setShowModal] = useState(false);
const [remainingTime, setRemainingTime] = useState(0);
const [hasAcknowledged, setHasAcknowledged] = useState(false);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const countdownRef = useRef<NodeJS.Timeout | null>(null);
// Check session status from server
const checkSessionStatus = useCallback(async () => {
// Get fresh token from store (not stale closure value) to avoid race condition
const currentToken = getAuthToken();
if (!isAuthenticated || !currentToken) return;
try {
const response = await fetch('/api/v1/auth/session/status', {
headers: {
'Authorization': `Bearer ${currentToken}`,
'Content-Type': 'application/json',
},
});
if (response.status === 401 || response.status === 503) {
// Session expired or service unavailable - logout
console.log('[SessionMonitor] Session invalid, logging out');
logout('expired');
return;
}
if (!response.ok) {
console.warn('[SessionMonitor] Session check failed:', response.status);
return;
}
const data = await response.json();
if (!data.is_valid) {
// Session expired
console.log('[SessionMonitor] Server says session invalid');
logout('expired');
return;
}
// Update remaining time (use absolute timeout for display)
if (data.absolute_seconds_remaining !== null) {
setRemainingTime(data.absolute_seconds_remaining);
}
// Show warning if needed (and user hasn't already acknowledged)
if (data.show_warning && !showModal && !hasAcknowledged) {
console.log('[SessionMonitor] Absolute timeout approaching, showing notice');
setShowModal(true);
}
} catch (error) {
console.error('[SessionMonitor] Error checking session:', error);
// On network error, don't logout immediately - wait for next poll
}
}, [isAuthenticated, showModal, hasAcknowledged, logout]);
// Start/stop polling based on auth state
useEffect(() => {
if (!hasHydrated || !isAuthenticated) {
// Clear any existing interval and reset state
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
setHasAcknowledged(false); // Reset on logout
return;
}
// Initial check
checkSessionStatus();
// Start polling
intervalRef.current = setInterval(checkSessionStatus, POLL_INTERVAL_MS);
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [hasHydrated, isAuthenticated, checkSessionStatus]);
// Countdown timer when modal is shown
useEffect(() => {
if (showModal && remainingTime > 0) {
countdownRef.current = setInterval(() => {
setRemainingTime(prev => {
const newTime = prev - 1;
if (newTime <= 0) {
// Time's up - logout
setShowModal(false);
logout('expired');
return 0;
}
return newTime;
});
}, 1000);
}
return () => {
if (countdownRef.current) {
clearInterval(countdownRef.current);
countdownRef.current = null;
}
};
}, [showModal, logout]);
// Handle "I Understand" button - just dismiss the notice
const handleAcknowledge = useCallback(() => {
console.log('[SessionMonitor] User acknowledged session expiration notice');
setShowModal(false);
setHasAcknowledged(true);
}, []);
return (
<>
{children}
<SessionTimeoutModal
open={showModal}
remainingTime={remainingTime}
onAcknowledge={handleAcknowledge}
/>
</>
);
}