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:
Dries Peeters
2025-10-13 12:51:23 +02:00
parent f2160df62e
commit e61c628526
6 changed files with 135 additions and 183 deletions
+24 -172
View File
@@ -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.
+30
View File
@@ -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
+15
View File
@@ -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
View File
@@ -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(_) {} });
-7
View File
@@ -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:
+1 -1
View File
@@ -7,7 +7,7 @@ from setuptools import setup, find_packages
setup(
name='timetracker',
version='2.3.4',
version='2.3.5',
packages=find_packages(),
include_package_data=True,
install_requires=[