Foro: correcciones de la revisión de seguridad y correctitud

- CSRF real en las vistas mutadoras con @csrf_protect (el middleware global no
  estaba activo; verificado: POST sin token -> 403). Se documenta el hueco global.
- bloquea acceso a temas de foros ocultos/borrados por URL directa (view_topic y reply)
- conteo de posts respeta borrados para moderadores (paginación correcta)
- tema bloqueado impide editar/borrar posts a no-moderadores (_topic_locked_for)
- moderador ve el formulario de respuesta en temas bloqueados (can_reply)
- filtro forum_safe: sanea el HTML también al renderizar (defensa en profundidad)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 17:34:03 +00:00
parent 071ba031a1
commit 89ea16ba4d
6 changed files with 81 additions and 14 deletions
+44 -8
View File
@@ -12,6 +12,7 @@ from django.contrib import messages
from django.http import JsonResponse
from django.shortcuts import redirect, render
from django.urls import reverse
from django.views.decorators.csrf import csrf_protect
from . import db
from . import permissions as perm
@@ -54,6 +55,14 @@ def _require_login(request):
return None
def _topic_locked_for(request, topic_id):
"""True si el tema está bloqueado y el usuario no es moderador."""
if perm.is_moderator(request):
return False
topic = db.get_topic(topic_id, include_hidden=True)
return bool(topic and topic['locked'])
# --------------------------------------------------------------------------
# Navegación
# --------------------------------------------------------------------------
@@ -108,12 +117,18 @@ def search(request):
def view_topic(request, topic_id, page=1):
topic = db.get_topic(topic_id, include_hidden=perm.is_moderator(request))
is_mod = perm.is_moderator(request)
topic = db.get_topic(topic_id, include_hidden=is_mod)
if not topic:
messages.error(request, 'Tema no encontrado.')
return redirect('app_forum')
forum = db.get_forum(topic['forum_id'], include_hidden=True)
# El foro padre debe ser visible (salvo moderador); si no, no se accede al
# tema por URL directa aunque no aparezca en los listados.
forum = db.get_forum(topic['forum_id'], include_hidden=is_mod)
if not forum:
messages.error(request, 'Tema no disponible.')
return redirect('app_forum')
# Contador de visitas (solo si la columna existe)
if db.has_views_column():
@@ -123,10 +138,9 @@ def view_topic(request, topic_id, page=1):
else:
topic['has_views'] = False
total = db.count_posts(topic_id)
total = db.count_posts(topic_id, include_deleted=is_mod)
pag, offset = _paginate(total, page, settings.FORUM_POSTS_PER_PAGE)
include_deleted = perm.is_moderator(request)
posts = db.get_posts(topic_id, offset, settings.FORUM_POSTS_PER_PAGE, include_deleted)
posts = db.get_posts(topic_id, offset, settings.FORUM_POSTS_PER_PAGE, is_mod)
# Marcar como leído
db.mark_read(perm.get_account_id(request), topic_id)
@@ -141,8 +155,8 @@ def view_topic(request, topic_id, page=1):
'forum': forum,
'posts': posts,
'pagination': pag,
'can_reply': perm.can_post(request) and not topic['locked'],
'move_forums': db.list_forums_for_move(topic['forum_id']) if perm.is_moderator(request) else [],
'can_reply': perm.can_post(request) and (not topic['locked'] or is_mod),
'move_forums': db.list_forums_for_move(topic['forum_id']) if is_mod else [],
'reply_form': ReplyForm(),
}))
@@ -165,6 +179,7 @@ def create_topic(request, forum_id):
}))
@csrf_protect
def submit_create_topic(request, forum_id):
login = _require_login(request)
if login:
@@ -193,6 +208,7 @@ def submit_create_topic(request, forum_id):
return redirect('forum_topic_p1', topic_id=topic_id)
@csrf_protect
def reply_to_topic(request, topic_id):
login = _require_login(request)
if login:
@@ -200,11 +216,16 @@ def reply_to_topic(request, topic_id):
if request.method != 'POST':
return redirect('forum_topic_p1', topic_id=topic_id)
is_mod = perm.is_moderator(request)
topic = db.get_topic(topic_id)
if not topic:
messages.error(request, 'Tema no encontrado.')
return redirect('app_forum')
if topic['locked'] and not perm.is_moderator(request):
# El foro padre debe ser visible (salvo moderador)
if not db.get_forum(topic['forum_id'], include_hidden=is_mod):
messages.error(request, 'Tema no disponible.')
return redirect('app_forum')
if topic['locked'] and not is_mod:
messages.error(request, 'El tema está bloqueado.')
return redirect('forum_topic_p1', topic_id=topic_id)
@@ -243,6 +264,7 @@ def edit_post(request, post_id):
}))
@csrf_protect
def update_post(request, post_id):
login = _require_login(request)
if login:
@@ -254,6 +276,9 @@ def update_post(request, post_id):
if not perm.can_edit_post(request, post):
messages.error(request, 'No tienes permiso para editar este post.')
return redirect('forum_topic_p1', topic_id=post['topic_id'])
if _topic_locked_for(request, post['topic_id']):
messages.error(request, 'El tema está bloqueado.')
return redirect('forum_topic_p1', topic_id=post['topic_id'])
if request.method != 'POST':
return redirect('forum_edit_post', post_id=post_id)
@@ -267,6 +292,7 @@ def update_post(request, post_id):
return redirect('forum_topic_p1', topic_id=post['topic_id'])
@csrf_protect
def delete_post(request, post_id):
login = _require_login(request)
if login:
@@ -278,6 +304,9 @@ def delete_post(request, post_id):
if not perm.can_edit_post(request, post):
messages.error(request, 'No tienes permiso.')
return redirect('forum_topic_p1', topic_id=post['topic_id'])
if _topic_locked_for(request, post['topic_id']):
messages.error(request, 'El tema está bloqueado.')
return redirect('forum_topic_p1', topic_id=post['topic_id'])
if request.method != 'POST':
return redirect('forum_topic_p1', topic_id=post['topic_id'])
db.set_post_deleted(post_id, True)
@@ -285,6 +314,7 @@ def delete_post(request, post_id):
return redirect('forum_topic_p1', topic_id=post['topic_id'])
@csrf_protect
def restore_post(request, post_id):
if not perm.is_moderator(request):
return redirect('app_forum')
@@ -319,6 +349,7 @@ def _mod_post_only(request):
return None
@csrf_protect
def lock_topic(request, topic_id):
block = _mod_post_only(request)
if block:
@@ -328,6 +359,7 @@ def lock_topic(request, topic_id):
return redirect('forum_topic_p1', topic_id=topic_id)
@csrf_protect
def unlock_topic(request, topic_id):
block = _mod_post_only(request)
if block:
@@ -337,6 +369,7 @@ def unlock_topic(request, topic_id):
return redirect('forum_topic_p1', topic_id=topic_id)
@csrf_protect
def toggle_sticky(request, topic_id):
block = _mod_post_only(request)
if block:
@@ -348,6 +381,7 @@ def toggle_sticky(request, topic_id):
return redirect('forum_topic_p1', topic_id=topic_id)
@csrf_protect
def delete_topic(request, topic_id):
block = _mod_post_only(request)
if block:
@@ -360,6 +394,7 @@ def delete_topic(request, topic_id):
return redirect('app_forum')
@csrf_protect
def restore_topic(request, topic_id):
block = _mod_post_only(request)
if block:
@@ -369,6 +404,7 @@ def restore_topic(request, topic_id):
return redirect('forum_topic_p1', topic_id=topic_id)
@csrf_protect
def move_topic(request, topic_id):
block = _mod_only(request)
if block: