V1.3
This commit is contained in:
60
app.py
60
app.py
@@ -44,7 +44,7 @@ DB_PASSWORD = load_secret('DB_DW_PASS')
|
||||
app.config.update(
|
||||
SESSION_COOKIE_HTTPONLY=True,
|
||||
SESSION_COOKIE_SECURE=True,
|
||||
SESSION_COOKIE_SAMESITE='Lax',
|
||||
SESSION_COOKIE_SAMESITE='Strict', # ← FIX: Strict au lieu de Lax
|
||||
MAX_CONTENT_LENGTH=16 * 1024 * 1024, # 16MB max
|
||||
PERMANENT_SESSION_LIFETIME=datetime.timedelta(hours=1),
|
||||
# Protection contre les attaques de session
|
||||
@@ -83,12 +83,31 @@ limiter = Limiter(
|
||||
storage_uri="memory://"
|
||||
)
|
||||
|
||||
# --- SÉCURITÉ : HEADERS ---
|
||||
@app.after_request
|
||||
def set_security_headers(response):
|
||||
"""Ajoute les headers de sécurité"""
|
||||
response.headers['Content-Security-Policy'] = (
|
||||
"default-src 'self'; "
|
||||
"img-src 'self'; "
|
||||
"style-src 'self' 'unsafe-inline'; "
|
||||
"script-src 'self'"
|
||||
)
|
||||
response.headers['X-Content-Type-Options'] = 'nosniff'
|
||||
response.headers['X-Frame-Options'] = 'DENY'
|
||||
response.headers['X-XSS-Protection'] = '1; mode=block'
|
||||
return response
|
||||
|
||||
# --- FONCTIONS UTILITAIRES SÉCURISÉES ---
|
||||
|
||||
def get_real_ip():
|
||||
"""Récupère l'IP réelle derrière le proxy Traefik"""
|
||||
return request.remote_addr or '0.0.0.0'
|
||||
|
||||
def hash_ip(ip):
|
||||
"""Hash l'IP pour anonymisation (RGPD-friendly)"""
|
||||
return hashlib.sha256(f"{ip}{app.secret_key}".encode()).hexdigest()[:8]
|
||||
|
||||
def validate_image_file(file_stream):
|
||||
"""
|
||||
Validation stricte du fichier image avec python-magic
|
||||
@@ -212,11 +231,12 @@ def index():
|
||||
|
||||
if request.method == 'POST':
|
||||
client_ip = get_real_ip()
|
||||
hashed_ip = hash_ip(client_ip) # ← FIX: Hash l'IP
|
||||
|
||||
# Vérification Anti-Spam
|
||||
cursor.execute(
|
||||
"SELECT created_at FROM posts WHERE ip_address = %s ORDER BY created_at DESC LIMIT 1",
|
||||
(client_ip,)
|
||||
(hashed_ip,)
|
||||
)
|
||||
last_post = cursor.fetchone()
|
||||
|
||||
@@ -273,7 +293,7 @@ def index():
|
||||
try:
|
||||
cursor.execute(
|
||||
"INSERT INTO posts (content, image_filename, ip_address) VALUES (%s, %s, %s)",
|
||||
(content, filename, client_ip)
|
||||
(content, filename, hashed_ip) # ← FIX: Utilise hashed_ip
|
||||
)
|
||||
conn.commit()
|
||||
success = "Message posté avec succès !"
|
||||
@@ -330,6 +350,23 @@ def index():
|
||||
font-weight: 500;
|
||||
}
|
||||
nav a:hover { text-decoration: underline; }
|
||||
nav form {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
}
|
||||
nav button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #0066cc;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
padding: 0;
|
||||
}
|
||||
nav button:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.flag {
|
||||
background: #fff3cd;
|
||||
padding: 8px 12px;
|
||||
@@ -378,7 +415,7 @@ def index():
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
}
|
||||
button {
|
||||
button[type="submit"] {
|
||||
background: #0066cc;
|
||||
color: white;
|
||||
border: none;
|
||||
@@ -389,8 +426,8 @@ def index():
|
||||
font-weight: 500;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
button:hover { background: #0052a3; }
|
||||
button:active { transform: scale(0.98); }
|
||||
button[type="submit"]:hover { background: #0052a3; }
|
||||
button[type="submit"]:active { transform: scale(0.98); }
|
||||
.post {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
@@ -424,7 +461,10 @@ def index():
|
||||
<div>
|
||||
<a href="/">🏠 Accueil</a>
|
||||
{% if session.get('logged_in') %}
|
||||
<a href="/logout">🚪 Déconnexion</a>
|
||||
<form method="post" action="{{ url_for('logout') }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||
<button type="submit">🚪 Déconnexion</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<a href="/login">🔐 Connexion Admin</a>
|
||||
{% endif %}
|
||||
@@ -469,11 +509,11 @@ def index():
|
||||
{% for post in posts %}
|
||||
<div class="post">
|
||||
<div class="meta">
|
||||
👤 <b>{{ post.ip_address }}</b> •
|
||||
👤 <b>User-{{ post.ip_address }}</b> •
|
||||
📅 {{ post.created_at.strftime('%d/%m/%Y à %H:%M') }}
|
||||
</div>
|
||||
{% if post.content %}
|
||||
<div class="post-content">{{ post.content }}</div>
|
||||
<div class="post-content">{{ post.content | e }}</div>
|
||||
{% endif %}
|
||||
{% if post.image_filename %}
|
||||
<img
|
||||
@@ -689,7 +729,7 @@ def login():
|
||||
|
||||
return render_template_string(html, error=error)
|
||||
|
||||
@app.route('/logout')
|
||||
@app.route('/logout', methods=['POST']) # ← FIX: Force POST
|
||||
def logout():
|
||||
"""Déconnexion sécurisée"""
|
||||
session.clear()
|
||||
|
||||
Reference in New Issue
Block a user