Security hardening
This commit is contained in:
+12
-1
@@ -1,4 +1,15 @@
|
|||||||
.env
|
.env
|
||||||
applications/
|
applications/
|
||||||
.coverage
|
.coverage
|
||||||
__pycache__
|
|
||||||
|
# Python cache files
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
|
.Python
|
||||||
|
*.so
|
||||||
|
*.egg
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
Binary file not shown.
Binary file not shown.
@@ -2,21 +2,63 @@ import os
|
|||||||
import uuid
|
import uuid
|
||||||
import yaml
|
import yaml
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from flask import Flask, render_template, request, redirect, url_for, flash, session
|
from flask import Flask, render_template, request, redirect, url_for, flash, session
|
||||||
from flask_mail import Mail, Message
|
from flask_mail import Mail, Message
|
||||||
|
from flask_wtf.csrf import CSRFProtect, CSRFError
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from config import Config
|
from config import Config
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config.from_object(Config)
|
app.config.from_object(Config)
|
||||||
mail = Mail(app)
|
mail = Mail(app)
|
||||||
|
csrf = CSRFProtect(app)
|
||||||
|
|
||||||
# Ensure applications folder exists
|
# Ensure applications folder exists
|
||||||
Path(app.config['APPLICATIONS_FOLDER']).mkdir(exist_ok=True)
|
Path(app.config['APPLICATIONS_FOLDER']).mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
# Rate limiting helper
|
||||||
|
def check_rate_limit():
|
||||||
|
"""
|
||||||
|
Check if user is submitting forms too quickly.
|
||||||
|
Returns (allowed: bool, wait_seconds: int)
|
||||||
|
"""
|
||||||
|
now = datetime.now()
|
||||||
|
last_submit_str = session.get('last_submission_time')
|
||||||
|
|
||||||
|
if last_submit_str:
|
||||||
|
try:
|
||||||
|
last_submit = datetime.fromisoformat(last_submit_str)
|
||||||
|
elapsed = (now - last_submit).total_seconds()
|
||||||
|
|
||||||
|
if elapsed < app.config['RATE_LIMIT_SECONDS']:
|
||||||
|
wait_seconds = int(app.config['RATE_LIMIT_SECONDS'] - elapsed) + 1
|
||||||
|
return False, wait_seconds
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
# Invalid timestamp, allow submission
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Update last submission time
|
||||||
|
session['last_submission_time'] = now.isoformat()
|
||||||
|
return True, 0
|
||||||
|
|
||||||
|
|
||||||
|
# Error handlers
|
||||||
|
@app.errorhandler(CSRFError)
|
||||||
|
def handle_csrf_error(e):
|
||||||
|
"""Handle CSRF validation errors."""
|
||||||
|
flash('Sicherheitsfehler: Die Sitzung ist abgelaufen. Bitte laden Sie die Seite neu und versuchen Sie es erneut.', 'error')
|
||||||
|
return redirect(url_for('page1_email')), 400
|
||||||
|
|
||||||
|
|
||||||
|
@app.errorhandler(429)
|
||||||
|
def handle_rate_limit_error(e):
|
||||||
|
"""Handle rate limit errors."""
|
||||||
|
return str(e), 429
|
||||||
|
|
||||||
|
|
||||||
def get_application_path(session_id):
|
def get_application_path(session_id):
|
||||||
"""Get the path to an application folder"""
|
"""Get the path to an application folder"""
|
||||||
return os.path.join(app.config['APPLICATIONS_FOLDER'], session_id)
|
return os.path.join(app.config['APPLICATIONS_FOLDER'], session_id)
|
||||||
@@ -137,6 +179,12 @@ def page1_email():
|
|||||||
@app.route('/apply/submit-email', methods=['POST'])
|
@app.route('/apply/submit-email', methods=['POST'])
|
||||||
def submit_email():
|
def submit_email():
|
||||||
"""Process email submission and create session"""
|
"""Process email submission and create session"""
|
||||||
|
# Check rate limit
|
||||||
|
allowed, wait_seconds = check_rate_limit()
|
||||||
|
if not allowed:
|
||||||
|
flash(f'Bitte warten Sie noch {wait_seconds} Sekunden vor der nächsten Eingabe.', 'error')
|
||||||
|
return redirect(url_for('page1_email')), 429
|
||||||
|
|
||||||
email = request.form.get('email', '').strip()
|
email = request.form.get('email', '').strip()
|
||||||
job_name = request.form.get('job_name', 'Offene Position')
|
job_name = request.form.get('job_name', 'Offene Position')
|
||||||
|
|
||||||
@@ -193,6 +241,12 @@ def page2_personal(session_id):
|
|||||||
@app.route('/apply/<session_id>/submit-personal', methods=['POST'])
|
@app.route('/apply/<session_id>/submit-personal', methods=['POST'])
|
||||||
def submit_personal(session_id):
|
def submit_personal(session_id):
|
||||||
"""Process personal information submission"""
|
"""Process personal information submission"""
|
||||||
|
# Check rate limit
|
||||||
|
allowed, wait_seconds = check_rate_limit()
|
||||||
|
if not allowed:
|
||||||
|
flash(f'Bitte warten Sie noch {wait_seconds} Sekunden vor der nächsten Eingabe.', 'error')
|
||||||
|
return redirect(url_for('page2_personal', session_id=session_id)), 429
|
||||||
|
|
||||||
app_data = load_application_data(session_id)
|
app_data = load_application_data(session_id)
|
||||||
if not app_data:
|
if not app_data:
|
||||||
flash('Bewerbung nicht gefunden.', 'error')
|
flash('Bewerbung nicht gefunden.', 'error')
|
||||||
@@ -276,6 +330,12 @@ def page3_motivation(session_id):
|
|||||||
@app.route('/apply/<session_id>/submit-motivation', methods=['POST'])
|
@app.route('/apply/<session_id>/submit-motivation', methods=['POST'])
|
||||||
def submit_motivation(session_id):
|
def submit_motivation(session_id):
|
||||||
"""Process motivation questions submission"""
|
"""Process motivation questions submission"""
|
||||||
|
# Check rate limit
|
||||||
|
allowed, wait_seconds = check_rate_limit()
|
||||||
|
if not allowed:
|
||||||
|
flash(f'Bitte warten Sie noch {wait_seconds} Sekunden vor der nächsten Eingabe.', 'error')
|
||||||
|
return redirect(url_for('page3_motivation', session_id=session_id)), 429
|
||||||
|
|
||||||
app_data = load_application_data(session_id)
|
app_data = load_application_data(session_id)
|
||||||
if not app_data:
|
if not app_data:
|
||||||
flash('Bewerbung nicht gefunden.', 'error')
|
flash('Bewerbung nicht gefunden.', 'error')
|
||||||
@@ -340,6 +400,12 @@ def page4_upload(session_id):
|
|||||||
@app.route('/apply/<session_id>/upload-file', methods=['POST'])
|
@app.route('/apply/<session_id>/upload-file', methods=['POST'])
|
||||||
def upload_file(session_id):
|
def upload_file(session_id):
|
||||||
"""Handle file upload"""
|
"""Handle file upload"""
|
||||||
|
# Check rate limit
|
||||||
|
allowed, wait_seconds = check_rate_limit()
|
||||||
|
if not allowed:
|
||||||
|
flash(f'Bitte warten Sie noch {wait_seconds} Sekunden vor der nächsten Eingabe.', 'error')
|
||||||
|
return redirect(url_for('page4_upload', session_id=session_id)), 429
|
||||||
|
|
||||||
app_data = load_application_data(session_id)
|
app_data = load_application_data(session_id)
|
||||||
if not app_data:
|
if not app_data:
|
||||||
flash('Bewerbung nicht gefunden.', 'error')
|
flash('Bewerbung nicht gefunden.', 'error')
|
||||||
@@ -437,6 +503,12 @@ def remove_file(session_id, file_index):
|
|||||||
@app.route('/apply/<session_id>/submit-application', methods=['POST'])
|
@app.route('/apply/<session_id>/submit-application', methods=['POST'])
|
||||||
def submit_application(session_id):
|
def submit_application(session_id):
|
||||||
"""Submit final application"""
|
"""Submit final application"""
|
||||||
|
# Check rate limit
|
||||||
|
allowed, wait_seconds = check_rate_limit()
|
||||||
|
if not allowed:
|
||||||
|
flash(f'Bitte warten Sie noch {wait_seconds} Sekunden vor der nächsten Eingabe.', 'error')
|
||||||
|
return redirect(url_for('page4_upload', session_id=session_id)), 429
|
||||||
|
|
||||||
app_data = load_application_data(session_id)
|
app_data = load_application_data(session_id)
|
||||||
if not app_data:
|
if not app_data:
|
||||||
flash('Bewerbung nicht gefunden.', 'error')
|
flash('Bewerbung nicht gefunden.', 'error')
|
||||||
|
|||||||
@@ -35,3 +35,14 @@ class Config:
|
|||||||
# Birth year validation
|
# Birth year validation
|
||||||
MIN_BIRTH_YEAR = 1940
|
MIN_BIRTH_YEAR = 1940
|
||||||
MAX_BIRTH_YEAR = 2010
|
MAX_BIRTH_YEAR = 2010
|
||||||
|
|
||||||
|
# Security settings
|
||||||
|
WTF_CSRF_ENABLED = True
|
||||||
|
WTF_CSRF_TIME_LIMIT = None # CSRF tokens don't expire (user can take time filling forms)
|
||||||
|
WTF_CSRF_SSL_STRICT = False # Set to True in production with HTTPS
|
||||||
|
SESSION_COOKIE_SECURE = False # Set to True in production (HTTPS only)
|
||||||
|
SESSION_COOKIE_HTTPONLY = True # Prevent JavaScript access to session cookie
|
||||||
|
SESSION_COOKIE_SAMESITE = 'Lax' # CSRF protection
|
||||||
|
|
||||||
|
# Rate limiting settings
|
||||||
|
RATE_LIMIT_SECONDS = 5 # Minimum seconds between form submissions
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
<objective>
|
||||||
|
Implement comprehensive security hardening for the Flask job application system by adding CSRF (Cross-Site Request Forgery) protection to all forms and implementing rate limiting to prevent abuse of form submissions.
|
||||||
|
|
||||||
|
This security enhancement protects against CSRF attacks where malicious sites trick users into submitting forms, and prevents automated abuse or DoS attacks through aggressive form submission. The rate limiting ensures a minimum 5-second delay between form submissions to prevent spam and abuse.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
The Flask job application system currently has 4 forms across the workflow:
|
||||||
|
- Page 1: Email submission form
|
||||||
|
- Page 2: Personal information form
|
||||||
|
- Page 3: Motivation questions form
|
||||||
|
- Page 4: File upload and final submission forms
|
||||||
|
|
||||||
|
Tech stack: Flask 3.0.0, Python 3, existing test suite with pytest
|
||||||
|
|
||||||
|
Current security gaps:
|
||||||
|
- No CSRF protection on forms (vulnerable to CSRF attacks)
|
||||||
|
- No rate limiting (vulnerable to automated submission abuse)
|
||||||
|
- Forms can be submitted repeatedly without delay
|
||||||
|
|
||||||
|
Examine these files to understand the current implementation:
|
||||||
|
@app.py - All routes and form handlers
|
||||||
|
@templates/*.html - All form templates
|
||||||
|
@config.py - Configuration settings
|
||||||
|
@tests/test_routes.py - Existing route tests that will need updating
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<requirements>
|
||||||
|
|
||||||
|
<csrf_protection>
|
||||||
|
**CSRF Token Implementation**
|
||||||
|
|
||||||
|
1. **Install and Configure Flask-WTF**:
|
||||||
|
- Add Flask-WTF to requirements.txt (provides CSRF protection)
|
||||||
|
- Configure CSRF protection in app.py
|
||||||
|
- Set secure CSRF configuration (token timeout, secure cookies)
|
||||||
|
|
||||||
|
2. **Add CSRF Tokens to All Forms**:
|
||||||
|
- Update all HTML templates to include CSRF tokens
|
||||||
|
- Add `{{ csrf_token() }}` hidden input to every form:
|
||||||
|
- templates/page1_email.html
|
||||||
|
- templates/page2_personal.html
|
||||||
|
- templates/page3_motivation.html
|
||||||
|
- templates/page4_upload.html (both upload form and submit form)
|
||||||
|
|
||||||
|
3. **Validate CSRF Tokens**:
|
||||||
|
- All POST routes must automatically validate CSRF tokens
|
||||||
|
- Return 400 Bad Request with clear error message on CSRF failure
|
||||||
|
- Ensure CSRF validation doesn't break the resume functionality
|
||||||
|
|
||||||
|
4. **CSRF Error Handling**:
|
||||||
|
- Add custom error handler for CSRF validation failures
|
||||||
|
- Display user-friendly German error messages
|
||||||
|
- Allow users to refresh and retry after CSRF errors
|
||||||
|
</csrf_protection>
|
||||||
|
|
||||||
|
<rate_limiting>
|
||||||
|
**Rate Limiting Implementation**
|
||||||
|
|
||||||
|
1. **Session-Based Rate Limiting**:
|
||||||
|
- Track last form submission timestamp in Flask session
|
||||||
|
- Enforce minimum 5-second delay between ANY form submissions
|
||||||
|
- Apply to all form POST routes:
|
||||||
|
- /apply/submit-email
|
||||||
|
- /apply/<session_id>/submit-personal
|
||||||
|
- /apply/<session_id>/submit-motivation
|
||||||
|
- /apply/<session_id>/upload-file
|
||||||
|
- /apply/<session_id>/submit-application
|
||||||
|
|
||||||
|
2. **Rate Limiting Logic**:
|
||||||
|
- Before processing any form submission, check session['last_submission_time']
|
||||||
|
- Calculate time elapsed since last submission
|
||||||
|
- If < 5 seconds, reject with HTTP 429 (Too Many Requests)
|
||||||
|
- If >= 5 seconds or first submission, allow and update timestamp
|
||||||
|
- Store timestamp in session for persistence across requests
|
||||||
|
|
||||||
|
3. **User-Friendly Rate Limit Messages**:
|
||||||
|
- Display remaining wait time in German
|
||||||
|
- Example: "Bitte warten Sie noch 3 Sekunden vor der nächsten Eingabe."
|
||||||
|
- Use flash messages to communicate rate limit errors
|
||||||
|
- Ensure message is clear and helps user understand the delay
|
||||||
|
|
||||||
|
4. **Rate Limit Exemptions**:
|
||||||
|
- GET requests are not rate limited (viewing pages)
|
||||||
|
- Resume links (loading existing applications) are not rate limited
|
||||||
|
- Only POST form submissions are rate limited
|
||||||
|
|
||||||
|
5. **Configuration**:
|
||||||
|
- Add RATE_LIMIT_SECONDS = 5 to config.py
|
||||||
|
- Make it configurable for different environments (dev, test, prod)
|
||||||
|
</rate_limiting>
|
||||||
|
|
||||||
|
<security_best_practices>
|
||||||
|
- Use secure session cookies (HttpOnly, Secure flags)
|
||||||
|
- Set proper SameSite attribute for cookies (Lax or Strict)
|
||||||
|
- Ensure CSRF tokens are cryptographically secure
|
||||||
|
- Rate limiting is per-session (prevents abuse from single source)
|
||||||
|
- Clear, informative error messages in German
|
||||||
|
- Maintain accessibility (forms remain usable with screen readers)
|
||||||
|
</security_best_practices>
|
||||||
|
|
||||||
|
</requirements>
|
||||||
|
|
||||||
|
<implementation>
|
||||||
|
|
||||||
|
**Implementation Steps**
|
||||||
|
|
||||||
|
1. **Update Dependencies**:
|
||||||
|
- Add Flask-WTF==1.2.1 to requirements.txt
|
||||||
|
- Flask-WTF provides CSRFProtect extension
|
||||||
|
|
||||||
|
2. **Configure CSRF Protection in app.py**:
|
||||||
|
```python
|
||||||
|
from flask_wtf.csrf import CSRFProtect, CSRFError
|
||||||
|
|
||||||
|
csrf = CSRFProtect(app)
|
||||||
|
app.config['WTF_CSRF_TIME_LIMIT'] = None # Or set to reasonable limit
|
||||||
|
app.config['WTF_CSRF_SSL_STRICT'] = True
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Add CSRF Tokens to Templates**:
|
||||||
|
- Add after opening `<form>` tag:
|
||||||
|
```html
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Implement Rate Limiting Decorator/Function**:
|
||||||
|
Create a helper function to check rate limits:
|
||||||
|
```python
|
||||||
|
def check_rate_limit():
|
||||||
|
"""Check if user is submitting forms too quickly."""
|
||||||
|
from datetime import datetime
|
||||||
|
from flask import session
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
last_submit = session.get('last_submission_time')
|
||||||
|
|
||||||
|
if last_submit:
|
||||||
|
# Parse stored timestamp and check elapsed time
|
||||||
|
# If < 5 seconds, return False and remaining wait time
|
||||||
|
# Else return True
|
||||||
|
|
||||||
|
session['last_submission_time'] = now.isoformat()
|
||||||
|
return True
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Update All POST Routes**:
|
||||||
|
- Add rate limit check at the beginning of each POST handler
|
||||||
|
- Return 429 error with flash message if rate limited
|
||||||
|
- CSRF validation happens automatically via Flask-WTF
|
||||||
|
|
||||||
|
6. **Add Error Handlers**:
|
||||||
|
```python
|
||||||
|
@app.errorhandler(CSRFError)
|
||||||
|
def handle_csrf_error(e):
|
||||||
|
flash('Sicherheitsfehler: Bitte laden Sie die Seite neu und versuchen Sie es erneut.', 'error')
|
||||||
|
return redirect(url_for('page1_email'))
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **Update Configuration**:
|
||||||
|
- Add rate limiting settings to config.py
|
||||||
|
- Ensure session configuration is secure
|
||||||
|
|
||||||
|
**What to Avoid and Why**:
|
||||||
|
- Don't disable CSRF for any POST routes - all forms need protection to prevent CSRF attacks
|
||||||
|
- Don't use IP-based rate limiting alone - users behind NAT/proxies share IPs; session-based is more accurate per-user
|
||||||
|
- Don't make rate limit too aggressive (< 5 seconds) - disrupts legitimate users trying to correct mistakes
|
||||||
|
- Don't forget to update tests - CSRF protection will break existing tests that don't include tokens
|
||||||
|
- Don't store sensitive data in rate limit error messages - only show wait time, not internal state
|
||||||
|
</implementation>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
Modify the following files:
|
||||||
|
|
||||||
|
1. `./requirements.txt` - Add Flask-WTF dependency
|
||||||
|
2. `./app.py` - Add CSRF protection, rate limiting logic, error handlers
|
||||||
|
3. `./config.py` - Add rate limiting and CSRF configuration
|
||||||
|
4. `./templates/page1_email.html` - Add CSRF token
|
||||||
|
5. `./templates/page2_personal.html` - Add CSRF token
|
||||||
|
6. `./templates/page3_motivation.html` - Add CSRF token
|
||||||
|
7. `./templates/page4_upload.html` - Add CSRF tokens (2 forms)
|
||||||
|
8. `./tests/test_routes.py` - Update tests to include CSRF tokens
|
||||||
|
9. `./tests/conftest.py` - Update test configuration to handle CSRF in tests
|
||||||
|
|
||||||
|
All modifications should maintain existing functionality while adding security layers.
|
||||||
|
</output>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring complete, verify your implementation:
|
||||||
|
|
||||||
|
1. **Test CSRF Protection**:
|
||||||
|
```bash
|
||||||
|
# Try submitting a form without CSRF token - should fail
|
||||||
|
curl -X POST http://localhost:5000/apply/submit-email -d "email=test@example.com"
|
||||||
|
```
|
||||||
|
Expected: 400 Bad Request or CSRF error
|
||||||
|
|
||||||
|
2. **Test Rate Limiting**:
|
||||||
|
- Submit a form successfully
|
||||||
|
- Immediately try to submit another form
|
||||||
|
- Should see rate limit error with wait time
|
||||||
|
- Wait 5 seconds and submit again - should succeed
|
||||||
|
|
||||||
|
3. **Run Test Suite**:
|
||||||
|
```bash
|
||||||
|
pytest -v tests/test_routes.py
|
||||||
|
```
|
||||||
|
All tests should pass with CSRF tokens included
|
||||||
|
|
||||||
|
4. **Manual Testing**:
|
||||||
|
- Start the application
|
||||||
|
- Complete a full application workflow
|
||||||
|
- Verify CSRF tokens are present in all forms (view page source)
|
||||||
|
- Try rapid form submission - should see rate limit message
|
||||||
|
- Verify error messages are in German
|
||||||
|
|
||||||
|
5. **Security Verification**:
|
||||||
|
- Check that cookies have Secure and HttpOnly flags
|
||||||
|
- Verify CSRF tokens are different for each request
|
||||||
|
- Confirm rate limiting persists across different forms
|
||||||
|
|
||||||
|
6. **Accessibility Check**:
|
||||||
|
- Forms still work with keyboard navigation
|
||||||
|
- Screen readers can still use forms
|
||||||
|
- Error messages are announced properly
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Flask-WTF installed and configured correctly
|
||||||
|
- CSRF tokens present in all 5 forms (view page source)
|
||||||
|
- All POST routes validate CSRF tokens automatically
|
||||||
|
- CSRF error handler displays German error message
|
||||||
|
- Rate limiting active on all form submissions
|
||||||
|
- Minimum 5-second delay enforced between submissions
|
||||||
|
- Rate limit error shows remaining wait time in German
|
||||||
|
- Session-based rate limiting works correctly
|
||||||
|
- All existing tests updated and passing
|
||||||
|
- Manual workflow test completes successfully
|
||||||
|
- Security best practices implemented (secure cookies, etc.)
|
||||||
|
- Error handling is user-friendly and in German
|
||||||
|
- No functionality broken by security additions
|
||||||
|
- Application remains accessible and usable
|
||||||
|
</success_criteria>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
Flask==3.0.0
|
Flask==3.0.0
|
||||||
PyYAML==6.0.1
|
PyYAML==6.0.1
|
||||||
Flask-Mail==0.9.1
|
Flask-Mail==0.9.1
|
||||||
|
Flask-WTF==1.2.1
|
||||||
python-dotenv==1.0.0
|
python-dotenv==1.0.0
|
||||||
Werkzeug==3.0.1
|
Werkzeug==3.0.1
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="POST" action="{{ url_for('submit_email') }}" class="application-form">
|
<form method="POST" action="{{ url_for('submit_email') }}" class="application-form">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<input type="hidden" name="job_name" value="{{ job_name }}">
|
<input type="hidden" name="job_name" value="{{ job_name }}">
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="POST" action="{{ url_for('submit_personal', session_id=session_id) }}" class="application-form">
|
<form method="POST" action="{{ url_for('submit_personal', session_id=session_id) }}" class="application-form">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
|
||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h3>Persönliche Informationen</h3>
|
<h3>Persönliche Informationen</h3>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form method="POST" action="{{ url_for('submit_motivation', session_id=session_id) }}" class="application-form">
|
<form method="POST" action="{{ url_for('submit_motivation', session_id=session_id) }}" class="application-form">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="current_job">Bitte beschreiben Sie Ihre aktuelle berufliche Situation.</label>
|
<label for="current_job">Bitte beschreiben Sie Ihre aktuelle berufliche Situation.</label>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
<span class="file-name">{{ file.original_name }}</span>
|
<span class="file-name">{{ file.original_name }}</span>
|
||||||
<span class="file-size">({{ "%.2f"|format(file.size / 1024 / 1024) }} MB)</span>
|
<span class="file-size">({{ "%.2f"|format(file.size / 1024 / 1024) }} MB)</span>
|
||||||
<form method="POST" action="{{ url_for('remove_file', session_id=session_id, file_index=loop.index0) }}" style="display: inline;">
|
<form method="POST" action="{{ url_for('remove_file', session_id=session_id, file_index=loop.index0) }}" style="display: inline;">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<button type="submit" class="btn btn-danger btn-small">Entfernen</button>
|
<button type="submit" class="btn btn-danger btn-small">Entfernen</button>
|
||||||
</form>
|
</form>
|
||||||
</li>
|
</li>
|
||||||
@@ -36,6 +37,7 @@
|
|||||||
<div class="upload-section">
|
<div class="upload-section">
|
||||||
<h3>Dokument hochladen</h3>
|
<h3>Dokument hochladen</h3>
|
||||||
<form method="POST" action="{{ url_for('upload_file', session_id=session_id) }}" enctype="multipart/form-data" class="upload-form">
|
<form method="POST" action="{{ url_for('upload_file', session_id=session_id) }}" enctype="multipart/form-data" class="upload-form">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="file">Datei auswählen</label>
|
<label for="file">Datei auswählen</label>
|
||||||
<input
|
<input
|
||||||
@@ -64,6 +66,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<form method="POST" action="{{ url_for('submit_application', session_id=session_id) }}">
|
<form method="POST" action="{{ url_for('submit_application', session_id=session_id) }}">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button type="submit" class="btn btn-primary btn-large">Bewerbung absenden</button>
|
<button type="submit" class="btn btn-primary btn-large">Bewerbung absenden</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ def app():
|
|||||||
flask_app.config['APPLICATIONS_FOLDER'] = temp_dir
|
flask_app.config['APPLICATIONS_FOLDER'] = temp_dir
|
||||||
flask_app.config['WTF_CSRF_ENABLED'] = False # Disable CSRF for testing
|
flask_app.config['WTF_CSRF_ENABLED'] = False # Disable CSRF for testing
|
||||||
flask_app.config['MAIL_SUPPRESS_SEND'] = True # Don't actually send emails
|
flask_app.config['MAIL_SUPPRESS_SEND'] = True # Don't actually send emails
|
||||||
|
flask_app.config['RATE_LIMIT_SECONDS'] = 0 # Disable rate limiting for testing
|
||||||
|
|
||||||
yield flask_app
|
yield flask_app
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user