findEthics commited on
Commit
bcc5440
·
1 Parent(s): f6278c5

Fix CSRF token issues for Hugging Face Spaces deployment

Browse files

- Add HuggingFaceConfig class with relaxed CSRF/session settings
- Disable SESSION_COOKIE_SECURE and WTF_CSRF_SSL_STRICT for HF Spaces
- Add enhanced CSRF error handling with debugging
- Remove duplicate error handlers
- Update deployment checklist with HF-specific configuration
- Set FLASK_ENV=huggingface for optimal HF Spaces compatibility

Files changed (3) hide show
  1. HUGGINGFACE_DEPLOYMENT_CHECKLIST.md +7 -7
  2. app.py +23 -8
  3. config.py +35 -4
HUGGINGFACE_DEPLOYMENT_CHECKLIST.md CHANGED
@@ -62,13 +62,13 @@
62
  SECRET_KEY=your-64-character-production-secret-key
63
  MONGODB_URL=your-mongodb-atlas-connection-string
64
  MONGODB_DATABASE=Atlas
65
- FLASK_ENV=production
66
  ```
67
 
68
- #### Security Variables:
69
  ```
70
- SESSION_COOKIE_SECURE=true
71
- WTF_CSRF_SSL_STRICT=true
72
  MAX_LOGIN_ATTEMPTS=3
73
  RATE_LIMIT_WINDOW=1800
74
  ```
@@ -285,9 +285,9 @@ Copy these to your Hugging Face Space settings:
285
  SECRET_KEY=your-generated-secret-key-here
286
  MONGODB_URL=mongodb+srv://username:password@cluster.mongodb.net/?retryWrites=true&w=majority&appName=Chatty
287
  MONGODB_DATABASE=Atlas
288
- FLASK_ENV=production
289
- SESSION_COOKIE_SECURE=true
290
- WTF_CSRF_SSL_STRICT=true
291
  MAX_LOGIN_ATTEMPTS=3
292
  RATE_LIMIT_WINDOW=1800
293
  SESSION_LIFETIME_HOURS=24
 
62
  SECRET_KEY=your-64-character-production-secret-key
63
  MONGODB_URL=your-mongodb-atlas-connection-string
64
  MONGODB_DATABASE=Atlas
65
+ FLASK_ENV=huggingface
66
  ```
67
 
68
+ #### Security Variables (Hugging Face Spaces optimized):
69
  ```
70
+ SESSION_COOKIE_SECURE=false
71
+ WTF_CSRF_SSL_STRICT=false
72
  MAX_LOGIN_ATTEMPTS=3
73
  RATE_LIMIT_WINDOW=1800
74
  ```
 
285
  SECRET_KEY=your-generated-secret-key-here
286
  MONGODB_URL=mongodb+srv://username:password@cluster.mongodb.net/?retryWrites=true&w=majority&appName=Chatty
287
  MONGODB_DATABASE=Atlas
288
+ FLASK_ENV=huggingface
289
+ SESSION_COOKIE_SECURE=false
290
+ WTF_CSRF_SSL_STRICT=false
291
  MAX_LOGIN_ATTEMPTS=3
292
  RATE_LIMIT_WINDOW=1800
293
  SESSION_LIFETIME_HOURS=24
app.py CHANGED
@@ -29,9 +29,30 @@ app = Flask(__name__)
29
  config_class = get_config()
30
  app.config.from_object(config_class)
31
 
32
- # CSRF Protection
33
  csrf = CSRFProtect(app)
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  # Configure logging
36
  logging.basicConfig(
37
  level=getattr(logging, app.config['LOG_LEVEL']),
@@ -398,13 +419,7 @@ def health():
398
  """
399
  return jsonify({"status": "healthy"})
400
 
401
- @app.errorhandler(400)
402
- def handle_bad_request(e):
403
- """Handle bad request errors including CSRF"""
404
- if 'CSRF' in str(e):
405
- flash('Security token expired. Please try again.', 'error')
406
- return redirect(request.url)
407
- return render_template('errors/400.html'), 400
408
 
409
  @app.errorhandler(401)
410
  def handle_unauthorized(e):
 
29
  config_class = get_config()
30
  app.config.from_object(config_class)
31
 
32
+ # CSRF Protection with error handling
33
  csrf = CSRFProtect(app)
34
 
35
+ # Add CSRF error handler for debugging
36
+ @app.errorhandler(400)
37
+ def handle_csrf_error(e):
38
+ """Handle CSRF errors with better debugging"""
39
+ error_description = str(e.description) if hasattr(e, 'description') else str(e)
40
+
41
+ # Log CSRF errors for debugging
42
+ app.logger.warning(f"CSRF Error: {error_description}")
43
+ app.logger.warning(f"Request headers: {dict(request.headers)}")
44
+ app.logger.warning(f"Request form: {dict(request.form)}")
45
+ app.logger.warning(f"Session: {dict(session)}")
46
+
47
+ if 'CSRF' in error_description:
48
+ flash('Security token expired. Please try again.', 'error')
49
+ # Redirect back to the same page to regenerate CSRF token
50
+ if request.endpoint in ['login', 'register']:
51
+ return redirect(url_for(request.endpoint))
52
+ return redirect(url_for('login'))
53
+
54
+ return render_template('errors/400.html'), 400
55
+
56
  # Configure logging
57
  logging.basicConfig(
58
  level=getattr(logging, app.config['LOG_LEVEL']),
 
419
  """
420
  return jsonify({"status": "healthy"})
421
 
422
+ # Error handler for 400 is already defined above with CSRF debugging
 
 
 
 
 
 
423
 
424
  @app.errorhandler(401)
425
  def handle_unauthorized(e):
config.py CHANGED
@@ -30,6 +30,10 @@ class Config:
30
  # CSRF Configuration
31
  WTF_CSRF_TIME_LIMIT = int(os.environ.get('CSRF_TIME_LIMIT', 3600)) # 1 hour
32
  WTF_CSRF_SSL_STRICT = os.environ.get('WTF_CSRF_SSL_STRICT', 'False').lower() == 'true'
 
 
 
 
33
 
34
  # Rate Limiting Configuration
35
  MAX_LOGIN_ATTEMPTS = int(os.environ.get('MAX_LOGIN_ATTEMPTS', 5))
@@ -73,8 +77,11 @@ class DevelopmentConfig(Config):
73
  class ProductionConfig(Config):
74
  """Production configuration"""
75
  DEBUG = False
76
- SESSION_COOKIE_SECURE = True
77
- WTF_CSRF_SSL_STRICT = True
 
 
 
78
 
79
  # Override with production-specific values
80
  MAX_LOGIN_ATTEMPTS = int(os.environ.get('MAX_LOGIN_ATTEMPTS', 3)) # Stricter in production
@@ -89,14 +96,37 @@ class ProductionConfig(Config):
89
  if not os.environ.get('SECRET_KEY'):
90
  errors.append("SECRET_KEY environment variable must be explicitly set in production")
91
 
92
- if not ProductionConfig.SESSION_COOKIE_SECURE:
93
- errors.append("SESSION_COOKIE_SECURE must be True in production (requires HTTPS)")
94
 
95
  if ProductionConfig.DEBUG:
96
  errors.append("DEBUG must be False in production")
97
 
98
  return errors
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  class TestingConfig(Config):
101
  """Testing configuration"""
102
  TESTING = True
@@ -109,6 +139,7 @@ class TestingConfig(Config):
109
  config = {
110
  'development': DevelopmentConfig,
111
  'production': ProductionConfig,
 
112
  'testing': TestingConfig,
113
  'default': DevelopmentConfig
114
  }
 
30
  # CSRF Configuration
31
  WTF_CSRF_TIME_LIMIT = int(os.environ.get('CSRF_TIME_LIMIT', 3600)) # 1 hour
32
  WTF_CSRF_SSL_STRICT = os.environ.get('WTF_CSRF_SSL_STRICT', 'False').lower() == 'true'
33
+ WTF_CSRF_ENABLED = os.environ.get('WTF_CSRF_ENABLED', 'True').lower() == 'true'
34
+
35
+ # Additional CSRF settings for proxy environments (like Hugging Face Spaces)
36
+ WTF_CSRF_CHECK_DEFAULT = os.environ.get('WTF_CSRF_CHECK_DEFAULT', 'True').lower() == 'true'
37
 
38
  # Rate Limiting Configuration
39
  MAX_LOGIN_ATTEMPTS = int(os.environ.get('MAX_LOGIN_ATTEMPTS', 5))
 
77
  class ProductionConfig(Config):
78
  """Production configuration"""
79
  DEBUG = False
80
+
81
+ # Hugging Face Spaces compatibility
82
+ # HF Spaces runs behind a proxy, so we need to be more flexible with CSRF/session settings
83
+ SESSION_COOKIE_SECURE = os.environ.get('SESSION_COOKIE_SECURE', 'False').lower() == 'true'
84
+ WTF_CSRF_SSL_STRICT = os.environ.get('WTF_CSRF_SSL_STRICT', 'False').lower() == 'true'
85
 
86
  # Override with production-specific values
87
  MAX_LOGIN_ATTEMPTS = int(os.environ.get('MAX_LOGIN_ATTEMPTS', 3)) # Stricter in production
 
96
  if not os.environ.get('SECRET_KEY'):
97
  errors.append("SECRET_KEY environment variable must be explicitly set in production")
98
 
99
+ # Relaxed validation for Hugging Face Spaces compatibility
100
+ # SESSION_COOKIE_SECURE and WTF_CSRF_SSL_STRICT are now optional in production
101
 
102
  if ProductionConfig.DEBUG:
103
  errors.append("DEBUG must be False in production")
104
 
105
  return errors
106
 
107
+ class HuggingFaceConfig(Config):
108
+ """Hugging Face Spaces specific configuration"""
109
+ DEBUG = False
110
+
111
+ # Hugging Face Spaces runs behind proxies, so we need relaxed settings
112
+ SESSION_COOKIE_SECURE = False # HF handles HTTPS at proxy level
113
+ WTF_CSRF_SSL_STRICT = False # Don't enforce SSL for CSRF
114
+ SESSION_COOKIE_SAMESITE = 'Lax' # More permissive for proxy environments
115
+
116
+ # Production-level security for other settings
117
+ MAX_LOGIN_ATTEMPTS = int(os.environ.get('MAX_LOGIN_ATTEMPTS', 3))
118
+ RATE_LIMIT_WINDOW = int(os.environ.get('RATE_LIMIT_WINDOW', 1800))
119
+
120
+ @staticmethod
121
+ def validate_config():
122
+ """Validation for Hugging Face Spaces"""
123
+ errors = Config.validate_config()
124
+
125
+ if not os.environ.get('SECRET_KEY'):
126
+ errors.append("SECRET_KEY environment variable must be explicitly set")
127
+
128
+ return errors
129
+
130
  class TestingConfig(Config):
131
  """Testing configuration"""
132
  TESTING = True
 
139
  config = {
140
  'development': DevelopmentConfig,
141
  'production': ProductionConfig,
142
+ 'huggingface': HuggingFaceConfig,
143
  'testing': TestingConfig,
144
  'default': DevelopmentConfig
145
  }