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:
90
apps/control-panel-backend/app/models/session.py
Normal file
90
apps/control-panel-backend/app/models/session.py
Normal file
@@ -0,0 +1,90 @@
|
||||
"""
|
||||
Session database model for server-side session tracking.
|
||||
|
||||
OWASP/NIST Compliant Session Management (Issue #264):
|
||||
- Server-side session state is authoritative
|
||||
- Tracks idle timeout (30 min) and absolute timeout (8 hours)
|
||||
- Session token hash stored (never plaintext)
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, Any
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, ForeignKey
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
import uuid
|
||||
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class Session(Base):
|
||||
"""Server-side session model for OWASP/NIST compliant session management"""
|
||||
|
||||
__tablename__ = "sessions"
|
||||
|
||||
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
||||
user_id = Column(Integer, ForeignKey("users.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
session_token_hash = Column(String(64), unique=True, nullable=False, index=True) # SHA-256 hash
|
||||
|
||||
# Session timing (NIST SP 800-63B compliant)
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
last_activity_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
absolute_expires_at = Column(DateTime(timezone=True), nullable=False)
|
||||
|
||||
# Session metadata for security auditing
|
||||
ip_address = Column(String(45), nullable=True) # IPv6 compatible
|
||||
user_agent = Column(Text, nullable=True)
|
||||
tenant_id = Column(Integer, ForeignKey("tenants.id"), nullable=True, index=True)
|
||||
|
||||
# Session state
|
||||
is_active = Column(Boolean, default=True, nullable=False)
|
||||
revoked_at = Column(DateTime(timezone=True), nullable=True)
|
||||
revoke_reason = Column(String(50), nullable=True) # 'logout', 'idle_timeout', 'absolute_timeout', 'admin_revoke', 'password_change', 'cleanup_stale'
|
||||
ended_at = Column(DateTime(timezone=True), nullable=True) # When session ended (any reason: logout, timeout, etc.)
|
||||
app_type = Column(String(20), default='control_panel', nullable=False) # 'control_panel' or 'tenant_app'
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", back_populates="sessions")
|
||||
tenant = relationship("Tenant", backref="sessions")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<Session(id={self.id}, user_id={self.user_id}, is_active={self.is_active})>"
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert session to dictionary (excluding sensitive data)"""
|
||||
return {
|
||||
"id": str(self.id),
|
||||
"user_id": self.user_id,
|
||||
"tenant_id": self.tenant_id,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"last_activity_at": self.last_activity_at.isoformat() if self.last_activity_at else None,
|
||||
"absolute_expires_at": self.absolute_expires_at.isoformat() if self.absolute_expires_at else None,
|
||||
"ip_address": self.ip_address,
|
||||
"is_active": self.is_active,
|
||||
"revoked_at": self.revoked_at.isoformat() if self.revoked_at else None,
|
||||
"revoke_reason": self.revoke_reason,
|
||||
"ended_at": self.ended_at.isoformat() if self.ended_at else None,
|
||||
"app_type": self.app_type,
|
||||
}
|
||||
|
||||
@property
|
||||
def is_expired(self) -> bool:
|
||||
"""Check if session is expired (either idle or absolute)"""
|
||||
if not self.is_active:
|
||||
return True
|
||||
|
||||
now = datetime.now(self.absolute_expires_at.tzinfo) if self.absolute_expires_at.tzinfo else datetime.utcnow()
|
||||
|
||||
# Check absolute timeout
|
||||
if now >= self.absolute_expires_at:
|
||||
return True
|
||||
|
||||
# Check idle timeout (30 minutes)
|
||||
from datetime import timedelta
|
||||
idle_timeout = timedelta(minutes=30)
|
||||
idle_expires_at = self.last_activity_at + idle_timeout
|
||||
|
||||
if now >= idle_expires_at:
|
||||
return True
|
||||
|
||||
return False
|
||||
Reference in New Issue
Block a user