mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-12 23:39:17 -05:00
feat: enhance CSRF protection with double-submit cookie pattern
Implement comprehensive CSRF token management with cookie-based double-submit pattern to improve security and SPA compatibility. Changes: - Add CSRF cookie configuration in app/config.py * WTF_CSRF_SSL_STRICT for strict SSL validation in production * CSRF_COOKIE_NAME (default: XSRF-TOKEN) for framework compatibility * CSRF_COOKIE_SECURE inherits from SESSION_COOKIE_SECURE by default * CSRF_COOKIE_HTTPONLY, CSRF_COOKIE_SAMESITE, and CSRF_COOKIE_DOMAIN settings - Implement CSRF cookie handler in app/__init__.py * Set CSRF token in cookie after each request * Configure cookie with secure flags based on environment settings * Support for double-submit pattern and SPA frameworks - Add client-side CSRF token management in base.html * JavaScript utilities for token retrieval and validation * Cookie synchronization for frameworks that read XSRF-TOKEN * Auto-refresh mechanism for stale tokens (>15 minutes) * Pre-submit token validation and refresh * User notification for missing cookies/tokens - Clean up docker-compose.yml environment variables * Remove redundant SECRET_KEY, WTF_CSRF_*, and cookie security settings * These are now managed through .env files and config.py This enhancement provides better CSRF protection while maintaining compatibility with modern JavaScript frameworks and SPA architectures.
This commit is contained in:
+24
-172
@@ -1,178 +1,30 @@
|
||||
feat: Add customizable Kanban board columns and enhance CSRF configuration
|
||||
feat: enhance CSRF protection with double-submit cookie pattern
|
||||
|
||||
This commit introduces a comprehensive Kanban board customization system and
|
||||
improves CSRF token configuration for Docker deployments.
|
||||
Implement comprehensive CSRF token management with cookie-based
|
||||
double-submit pattern to improve security and SPA compatibility.
|
||||
|
||||
## Major Features
|
||||
Changes:
|
||||
- Add CSRF cookie configuration in app/config.py
|
||||
* WTF_CSRF_SSL_STRICT for strict SSL validation in production
|
||||
* CSRF_COOKIE_NAME (default: XSRF-TOKEN) for framework compatibility
|
||||
* CSRF_COOKIE_SECURE inherits from SESSION_COOKIE_SECURE by default
|
||||
* CSRF_COOKIE_HTTPONLY, CSRF_COOKIE_SAMESITE, and CSRF_COOKIE_DOMAIN settings
|
||||
|
||||
### 1. Customizable Kanban Board Columns
|
||||
Add complete kanban column customization system allowing users to define
|
||||
custom workflow states beyond the default columns.
|
||||
- Implement CSRF cookie handler in app/__init__.py
|
||||
* Set CSRF token in cookie after each request
|
||||
* Configure cookie with secure flags based on environment settings
|
||||
* Support for double-submit pattern and SPA frameworks
|
||||
|
||||
**New Components:**
|
||||
- Add KanbanColumn model with full CRUD operations (app/models/kanban_column.py)
|
||||
- Add kanban routes blueprint with admin endpoints (app/routes/kanban.py)
|
||||
- Add kanban column management templates (app/templates/kanban/)
|
||||
- Add migration 019 for kanban_columns table (migrations/)
|
||||
- Add client-side CSRF token management in base.html
|
||||
* JavaScript utilities for token retrieval and validation
|
||||
* Cookie synchronization for frameworks that read XSRF-TOKEN
|
||||
* Auto-refresh mechanism for stale tokens (>15 minutes)
|
||||
* Pre-submit token validation and refresh
|
||||
* User notification for missing cookies/tokens
|
||||
|
||||
**Features:**
|
||||
- Create unlimited custom columns with unique keys, labels, icons, and colors
|
||||
- Drag-and-drop column reordering with position persistence
|
||||
- Toggle column visibility without deletion
|
||||
- Protected system columns (todo, in_progress, done) prevent accidental deletion
|
||||
- Complete state marking for columns that should mark tasks as done
|
||||
- Real-time updates via SocketIO broadcasts when columns change
|
||||
- Font Awesome icon support (5000+ icons)
|
||||
- Bootstrap color scheme integration
|
||||
- Comprehensive validation and error handling
|
||||
- Clean up docker-compose.yml environment variables
|
||||
* Remove redundant SECRET_KEY, WTF_CSRF_*, and cookie security settings
|
||||
* These are now managed through .env files and config.py
|
||||
|
||||
**Integration:**
|
||||
- Update Task model to work with dynamic column statuses (app/models/task.py)
|
||||
- Update task routes to use kanban column API (app/routes/tasks.py)
|
||||
- Update project routes to fetch active columns (app/routes/projects.py)
|
||||
- Add kanban column management links to base template (app/templates/base.html)
|
||||
- Update kanban board templates to render dynamic columns (app/templates/tasks/)
|
||||
- Add cache prevention headers to force fresh column data
|
||||
|
||||
**API Endpoints:**
|
||||
- GET /api/kanban/columns - Fetch all active columns
|
||||
- POST /api/kanban/columns/reorder - Reorder columns
|
||||
- GET /kanban/columns - Column management interface (admin only)
|
||||
- POST /kanban/columns/create - Create new column (admin only)
|
||||
- POST /kanban/columns/<id>/edit - Edit column (admin only)
|
||||
- POST /kanban/columns/<id>/delete - Delete column (admin only)
|
||||
- POST /kanban/columns/<id>/toggle - Toggle column visibility (admin only)
|
||||
|
||||
### 2. Enhanced CSRF Configuration
|
||||
Improve CSRF token configuration and documentation for Docker deployments.
|
||||
|
||||
**Configuration Updates:**
|
||||
- Add WTF_CSRF_ENABLED environment variable to all docker-compose files
|
||||
- Add WTF_CSRF_TIME_LIMIT environment variable with 1-hour default
|
||||
- Update app/config.py to read CSRF settings from environment
|
||||
- Add SECRET_KEY validation in app/__init__.py to prevent production deployment
|
||||
with default keys
|
||||
|
||||
**Docker Compose Updates:**
|
||||
- docker-compose.yml: CSRF enabled by default for security testing
|
||||
- docker-compose.remote.yml: CSRF always enabled in production
|
||||
- docker-compose.remote-dev.yml: CSRF enabled with production-like settings
|
||||
- docker-compose.local-test.yml: CSRF can be disabled for local testing
|
||||
- Add helpful comments explaining each CSRF-related environment variable
|
||||
- Update env.example with CSRF configuration examples
|
||||
|
||||
**Verification Scripts:**
|
||||
- Add scripts/verify_csrf_config.sh for Unix systems
|
||||
- Add scripts/verify_csrf_config.bat for Windows systems
|
||||
- Scripts check SECRET_KEY, CSRF_ENABLED, and CSRF_TIME_LIMIT settings
|
||||
|
||||
### 3. Database Initialization Improvements
|
||||
- Update app/__init__.py to run pending migrations on startup
|
||||
- Add automatic kanban column initialization after migrations
|
||||
- Improve error handling and logging during database setup
|
||||
|
||||
### 4. Configuration Management
|
||||
- Update app/config.py with new CSRF and kanban-related settings
|
||||
- Add environment variable parsing with sensible defaults
|
||||
- Improve configuration validation and error messages
|
||||
|
||||
## Documentation
|
||||
|
||||
### New Documentation Files
|
||||
- CUSTOM_KANBAN_README.md: Quick start guide for kanban customization
|
||||
- KANBAN_CUSTOMIZATION.md: Detailed technical documentation
|
||||
- IMPLEMENTATION_SUMMARY.md: Implementation details and architecture
|
||||
- KANBAN_AUTO_REFRESH_COMPLETE.md: Real-time update system documentation
|
||||
- KANBAN_REFRESH_FINAL_FIX.md: Cache and refresh troubleshooting
|
||||
- KANBAN_REFRESH_SOLUTION.md: Technical solution for data freshness
|
||||
- docs/CSRF_CONFIGURATION.md: Comprehensive CSRF setup guide
|
||||
- CSRF_DOCKER_CONFIGURATION_SUMMARY.md: Docker-specific CSRF setup
|
||||
- CSRF_TROUBLESHOOTING.md: Common CSRF issues and solutions
|
||||
- APPLY_KANBAN_MIGRATION.md: Migration application guide
|
||||
- APPLY_FIXES_NOW.md: Quick fix reference
|
||||
- DEBUG_KANBAN_COLUMNS.md: Debugging guide
|
||||
- DIAGNOSIS_STEPS.md: System diagnosis procedures
|
||||
- BROWSER_CACHE_FIX.md: Browser cache troubleshooting
|
||||
- FORCE_NO_CACHE_FIX.md: Cache prevention solutions
|
||||
- SESSION_CLOSE_ERROR_FIX.md: Session handling fixes
|
||||
- QUICK_FIX.md: Quick reference for common fixes
|
||||
|
||||
### Updated Documentation
|
||||
- README.md: Add kanban customization feature description
|
||||
- Update project documentation with new features
|
||||
|
||||
## Testing
|
||||
|
||||
### New Test Files
|
||||
- test_kanban_refresh.py: Test kanban column refresh functionality
|
||||
|
||||
## Technical Details
|
||||
|
||||
**Database Changes:**
|
||||
- New table: kanban_columns with 11 columns
|
||||
- Indexes on: key, position
|
||||
- Default data: 4 system columns (todo, in_progress, review, done)
|
||||
- Support for both SQLite (development) and PostgreSQL (production)
|
||||
|
||||
**Real-Time Updates:**
|
||||
- SocketIO events: 'kanban_columns_updated' with action type
|
||||
- Automatic page refresh when columns are created/updated/deleted/reordered
|
||||
- Prevents stale data by expiring SQLAlchemy caches after changes
|
||||
|
||||
**Security:**
|
||||
- Admin-only access to column management
|
||||
- CSRF protection on all column mutation endpoints
|
||||
- API endpoints exempt from CSRF (use JSON and other auth mechanisms)
|
||||
- System column protection prevents data integrity issues
|
||||
- Validation prevents deletion of columns with active tasks
|
||||
|
||||
**Performance:**
|
||||
- Efficient querying with position-based ordering
|
||||
- Cached column data with cache invalidation on changes
|
||||
- No-cache headers on API responses to prevent stale data
|
||||
- Optimized database indexes for fast lookups
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
None. This is a fully backward-compatible addition.
|
||||
|
||||
Existing workflows continue to work with the default columns.
|
||||
Custom columns are opt-in via the admin interface.
|
||||
|
||||
## Migration Notes
|
||||
|
||||
1. Run migration 019 to create kanban_columns table
|
||||
2. Default columns are initialized automatically on first run
|
||||
3. No data migration needed for existing tasks
|
||||
4. Existing task statuses map to new column keys
|
||||
|
||||
## Environment Variables
|
||||
|
||||
New environment variables (all optional with defaults):
|
||||
- WTF_CSRF_ENABLED: Enable/disable CSRF protection (default: true)
|
||||
- WTF_CSRF_TIME_LIMIT: CSRF token expiration in seconds (default: 3600)
|
||||
- SECRET_KEY: Required in production, must be cryptographically secure
|
||||
|
||||
See env.example for complete configuration reference.
|
||||
|
||||
## Deployment Notes
|
||||
|
||||
1. Ensure SECRET_KEY is set to a secure value in production
|
||||
2. Keep WTF_CSRF_ENABLED=true in production
|
||||
3. Run migration 019 before starting the application
|
||||
4. Clear browser cache if kanban board doesn't update
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential future improvements documented in implementation notes:
|
||||
- Column-specific permissions and access control
|
||||
- Column templates for common workflows
|
||||
- Import/export of column configurations
|
||||
- Column usage analytics and reporting
|
||||
- Workflow automation based on column transitions
|
||||
|
||||
---
|
||||
|
||||
This commit represents a major enhancement to the task management system,
|
||||
providing flexibility for teams to define their own workflows while
|
||||
maintaining security and data integrity.
|
||||
This enhancement provides better CSRF protection while maintaining
|
||||
compatibility with modern JavaScript frameworks and SPA architectures.
|
||||
|
||||
@@ -379,6 +379,36 @@ def create_app(config=None):
|
||||
resp.headers["Cache-Control"] = "no-store, no-cache, must-revalidate, max-age=0"
|
||||
except Exception:
|
||||
pass
|
||||
# Also set/update a CSRF cookie for double-submit pattern and SPA helpers
|
||||
try:
|
||||
cookie_name = app.config.get("CSRF_COOKIE_NAME", "XSRF-TOKEN")
|
||||
# Derive defaults from session cookie flags if not explicitly set
|
||||
cookie_secure = bool(
|
||||
app.config.get(
|
||||
"CSRF_COOKIE_SECURE",
|
||||
app.config.get("SESSION_COOKIE_SECURE", False),
|
||||
)
|
||||
)
|
||||
cookie_httponly = bool(app.config.get("CSRF_COOKIE_HTTPONLY", False))
|
||||
cookie_samesite = app.config.get("CSRF_COOKIE_SAMESITE", "Lax")
|
||||
cookie_domain = app.config.get("CSRF_COOKIE_DOMAIN") or None
|
||||
cookie_path = app.config.get("CSRF_COOKIE_PATH", "/")
|
||||
try:
|
||||
max_age = int(app.config.get("WTF_CSRF_TIME_LIMIT", 3600))
|
||||
except Exception:
|
||||
max_age = 3600
|
||||
resp.set_cookie(
|
||||
cookie_name,
|
||||
token or "",
|
||||
max_age=max_age,
|
||||
secure=cookie_secure,
|
||||
httponly=cookie_httponly,
|
||||
samesite=cookie_samesite,
|
||||
domain=cookie_domain,
|
||||
path=cookie_path,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return resp
|
||||
|
||||
# Register blueprints
|
||||
|
||||
@@ -77,6 +77,17 @@ class Config:
|
||||
# CSRF protection
|
||||
WTF_CSRF_ENABLED = os.getenv('WTF_CSRF_ENABLED', 'true').lower() == 'true'
|
||||
WTF_CSRF_TIME_LIMIT = int(os.getenv('WTF_CSRF_TIME_LIMIT', 3600)) # Default: 1 hour
|
||||
# If true, rejects requests considered insecure for CSRF; keep strict in prod, relaxed in dev
|
||||
WTF_CSRF_SSL_STRICT = os.getenv('WTF_CSRF_SSL_STRICT', 'true').lower() == 'true'
|
||||
# CSRF cookie settings (for double-submit cookie pattern and SPA helpers)
|
||||
CSRF_COOKIE_NAME = os.getenv('CSRF_COOKIE_NAME', 'XSRF-TOKEN')
|
||||
CSRF_COOKIE_SECURE = os.getenv('CSRF_COOKIE_SECURE', '').lower()
|
||||
# default secure flag: inherit from SESSION_COOKIE_SECURE if unset
|
||||
CSRF_COOKIE_SECURE = (CSRF_COOKIE_SECURE == 'true') if CSRF_COOKIE_SECURE in ('true','false') else SESSION_COOKIE_SECURE
|
||||
CSRF_COOKIE_HTTPONLY = os.getenv('CSRF_COOKIE_HTTPONLY', 'false').lower() == 'true'
|
||||
CSRF_COOKIE_SAMESITE = os.getenv('CSRF_COOKIE_SAMESITE', 'Lax')
|
||||
CSRF_COOKIE_DOMAIN = os.getenv('CSRF_COOKIE_DOMAIN')
|
||||
CSRF_COOKIE_PATH = os.getenv('CSRF_COOKIE_PATH', '/')
|
||||
|
||||
# Security headers
|
||||
SECURITY_HEADERS = {
|
||||
@@ -120,6 +131,8 @@ class DevelopmentConfig(Config):
|
||||
)
|
||||
# CSRF can be overridden via env var, defaults to False for dev convenience
|
||||
WTF_CSRF_ENABLED = os.getenv('WTF_CSRF_ENABLED', 'false').lower() == 'true'
|
||||
# Relax SSL strictness by default in dev to avoid false negatives on http
|
||||
WTF_CSRF_SSL_STRICT = os.getenv('WTF_CSRF_SSL_STRICT', 'false').lower() == 'true'
|
||||
|
||||
class TestingConfig(Config):
|
||||
"""Testing configuration"""
|
||||
@@ -129,6 +142,7 @@ class TestingConfig(Config):
|
||||
SQLALCHEMY_DATABASE_URI = os.getenv('DATABASE_URL', 'sqlite:///:memory:')
|
||||
WTF_CSRF_ENABLED = False
|
||||
SECRET_KEY = 'test-secret-key'
|
||||
WTF_CSRF_SSL_STRICT = False
|
||||
|
||||
class ProductionConfig(Config):
|
||||
"""Production configuration"""
|
||||
@@ -137,6 +151,7 @@ class ProductionConfig(Config):
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
REMEMBER_COOKIE_SECURE = True
|
||||
WTF_CSRF_ENABLED = True
|
||||
WTF_CSRF_SSL_STRICT = True
|
||||
|
||||
# Configuration mapping
|
||||
config = {
|
||||
|
||||
+65
-3
@@ -341,6 +341,54 @@
|
||||
// Temporary implementation until toast-notifications.js loads
|
||||
console.log('Toast:', type, message);
|
||||
}
|
||||
// Show a persistent top banner when cookies are disabled or blocked
|
||||
(function(){
|
||||
try {
|
||||
var cookiesEnabled = navigator.cookieEnabled;
|
||||
if (!cookiesEnabled) {
|
||||
renderCookieWarningBanner();
|
||||
return;
|
||||
}
|
||||
// Double-check by attempting to set and read a test cookie
|
||||
document.cookie = 'tt_cookie_check=1; path=/';
|
||||
var hasTest = document.cookie.indexOf('tt_cookie_check=1') !== -1;
|
||||
if (!hasTest) {
|
||||
renderCookieWarningBanner();
|
||||
} else {
|
||||
// Clean up test cookie
|
||||
document.cookie = 'tt_cookie_check=; Max-Age=0; path=/';
|
||||
}
|
||||
} catch (e) {
|
||||
// If access throws, assume cookies are blocked
|
||||
renderCookieWarningBanner();
|
||||
}
|
||||
|
||||
function renderCookieWarningBanner(){
|
||||
try {
|
||||
var existing = document.getElementById('cookie-disabled-banner');
|
||||
if (existing) return;
|
||||
var banner = document.createElement('div');
|
||||
banner.id = 'cookie-disabled-banner';
|
||||
banner.setAttribute('role','status');
|
||||
banner.setAttribute('aria-live','polite');
|
||||
banner.style.position = 'fixed';
|
||||
banner.style.top = '0';
|
||||
banner.style.left = '0';
|
||||
banner.style.right = '0';
|
||||
banner.style.zIndex = '1100';
|
||||
banner.style.padding = '10px 16px';
|
||||
banner.style.background = '#b91c1c';
|
||||
banner.style.color = 'white';
|
||||
banner.style.display = 'flex';
|
||||
banner.style.alignItems = 'center';
|
||||
banner.style.justifyContent = 'center';
|
||||
banner.style.gap = '8px';
|
||||
banner.innerHTML = '<strong>Cookies are disabled or blocked.</strong> ' +
|
||||
'This app requires cookies for secure login and CSRF protection.';
|
||||
document.body.appendChild(banner);
|
||||
} catch (_) {}
|
||||
}
|
||||
})();
|
||||
|
||||
// Navbar scrolled shadow behavior
|
||||
document.addEventListener('scroll', function() {
|
||||
@@ -626,6 +674,14 @@
|
||||
}
|
||||
function setToken(t){
|
||||
if (meta && typeof t === 'string' && t) { meta.setAttribute('content', t); lastRefreshAt = Date.now(); }
|
||||
// Best-effort sync: put token into CSRF cookie for frameworks/tools that read it from cookies
|
||||
try {
|
||||
var cookieName = (document.documentElement.getAttribute('data-csrf-cookie-name') || 'XSRF-TOKEN');
|
||||
if (t && cookieName) {
|
||||
// Write a non-HttpOnly cookie; flags controlled server-side for secure contexts
|
||||
document.cookie = cookieName + '=' + encodeURIComponent(t) + '; path=/';
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
function isPostForm(form){
|
||||
var m = (form.getAttribute('method') || form.method || '').toString().toUpperCase();
|
||||
@@ -681,7 +737,11 @@
|
||||
if (sameOrigin && ['GET','HEAD','OPTIONS','TRACE'].indexOf(method) === -1) {
|
||||
var headers = new Headers(req.headers || {});
|
||||
if (!headers.has('X-CSRFToken')) headers.set('X-CSRFToken', getToken());
|
||||
return _origFetch(new Request(req, { headers: headers }));
|
||||
// Ensure cookies are sent with same-origin requests
|
||||
var opts = { headers: headers };
|
||||
// Preserve explicit credentials if caller set them; otherwise default to same-origin
|
||||
if (!('credentials' in req)) { opts.credentials = 'same-origin'; }
|
||||
return _origFetch(new Request(req, opts));
|
||||
}
|
||||
return _origFetch(req);
|
||||
} catch(e) {
|
||||
@@ -731,12 +791,14 @@
|
||||
}).observe(document.body, { childList: true, subtree: true });
|
||||
} catch(_) {}
|
||||
|
||||
// Pre-submit refresh if token is stale (>15 minutes old)
|
||||
// Pre-submit: if token missing or stale (>15 minutes), refresh before submit
|
||||
document.addEventListener('submit', function(ev){
|
||||
var form = ev.target;
|
||||
if (!form || form.tagName !== 'FORM') return;
|
||||
var now = Date.now();
|
||||
if (now - lastRefreshAt > 15 * 60 * 1000) {
|
||||
var token = getToken();
|
||||
var needsRefresh = !token || (now - lastRefreshAt > 15 * 60 * 1000);
|
||||
if (needsRefresh) {
|
||||
try {
|
||||
ev.preventDefault();
|
||||
refreshCsrfToken().then(function(){ try { form.submit(); } catch(_) {} });
|
||||
|
||||
@@ -20,15 +20,8 @@ services:
|
||||
# 4. If behind a reverse proxy, ensure it forwards cookies correctly
|
||||
# 5. Check the token hasn't expired (increase WTF_CSRF_TIME_LIMIT if needed)
|
||||
# For details: docs/CSRF_CONFIGURATION.md
|
||||
- SECRET_KEY=${SECRET_KEY:-your-secret-key-change-this}
|
||||
- DATABASE_URL=postgresql+psycopg2://timetracker:timetracker@db:5432/timetracker
|
||||
- LOG_FILE=/app/logs/timetracker.log
|
||||
# CSRF Protection (enabled by default for security)
|
||||
- WTF_CSRF_ENABLED=${WTF_CSRF_ENABLED:-true}
|
||||
- WTF_CSRF_TIME_LIMIT=${WTF_CSRF_TIME_LIMIT:-3600}
|
||||
# Ensure cookies work over HTTP (disable Secure for local/dev or non-TLS proxies)
|
||||
- SESSION_COOKIE_SECURE=${SESSION_COOKIE_SECURE:-false}
|
||||
- REMEMBER_COOKIE_SECURE=${REMEMBER_COOKIE_SECURE:-false}
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
|
||||
Reference in New Issue
Block a user