Spaces:
Sleeping
Sleeping
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
- HUGGINGFACE_DEPLOYMENT_CHECKLIST.md +7 -7
- app.py +23 -8
- 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=
|
| 66 |
```
|
| 67 |
|
| 68 |
-
#### Security Variables:
|
| 69 |
```
|
| 70 |
-
SESSION_COOKIE_SECURE=
|
| 71 |
-
WTF_CSRF_SSL_STRICT=
|
| 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=
|
| 289 |
-
SESSION_COOKIE_SECURE=
|
| 290 |
-
WTF_CSRF_SSL_STRICT=
|
| 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 |
-
|
| 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 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 93 |
-
|
| 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 |
}
|