fix: A10 — autoescape Jinja2 activé partout via un Environment partagé
FlowDeck CI / docker (push) Successful in 1m44s
FlowDeck CI / lint (push) Successful in 1m49s
FlowDeck CI / test (push) Successful in 21m54s

- `app/templating.py` : `ENV = Environment(loader=FileSystemLoader("app/templates"), autoescape=select_autoescape(["html"]))`
- les 29 instantiations `Environment(loader=FileSystemLoader("app/templates"))` (9 routers) remplacées par `env = ENV` — plus aucune interpolation `{{ … }}` servie crue, les `|safe` redeviennent efficaces
- re-tri des `|safe` : `card_detail.html` corps d'issue et commentaires échappés (XSS stocké), placeholder de description sorti du ternaire, `sidebar_config` passé en dict + `|tojson` (échappement `</script>` en contexte script)
- `|safe` conservé sur `right_actions` (HTML fabriqué dans les templates, fiable)
- `ruff check app tests` OK · suite **1016/1016 verts**
This commit is contained in:
2026-09-30 22:28:48 -04:00
parent d76d7943fc
commit 69a0aceba6
17 changed files with 117 additions and 74 deletions
+32 -36
View File
@@ -252,10 +252,9 @@ def _sidebar_data(request: Request, repos: list[dict], include_workspace: bool =
@router.get("/trash", response_class=HTMLResponse)
async def trash_page(request: Request, owner: str = Query(default=""), repo: str = Query(default="")):
"""Trash page — scoped to workspace if owner/repo provided."""
from jinja2 import Environment, FileSystemLoader
from app.routers.board import _sidebar_data as board_sidebar
env = Environment(loader=FileSystemLoader("app/templates"))
from app.templating import ENV
env = ENV
sidebar = board_sidebar(request, owner, repo)
with get_conn() as conn:
ws_key = f"{owner}/{repo}" if owner and repo else ""
@@ -283,10 +282,9 @@ async def library_page(request: Request, owner: str = Query(default=""), repo: s
Sidebar data is kept intact. Tab content is loaded client-side via /api/library/* endpoints.
"""
from jinja2 import Environment, FileSystemLoader
from app.routers.board import _sidebar_data as board_sidebar
env = Environment(loader=FileSystemLoader("app/templates"))
from app.templating import ENV
env = ENV
sidebar = board_sidebar(request, owner, repo)
# Pass active workspace for breadcrumb nav menu
ws_key_ws = f"{owner}/{repo}" if owner and repo else ""
@@ -312,10 +310,9 @@ async def view_page_root(request: Request, page_id: int):
"""Render a Markdown page at root level with workspace context — or file viewer.
?embed=1 — minimal mode for side peek (editor only, no header)."""
embed = request.query_params.get("embed") == "1"
from jinja2 import Environment, FileSystemLoader
from app.routers.board import _sidebar_data as board_sidebar
env = Environment(loader=FileSystemLoader("app/templates"))
from app.templating import ENV
env = ENV
with get_conn() as conn:
row = conn.execute("SELECT * FROM pages WHERE id=?", (page_id,)).fetchone()
if not row:
@@ -437,8 +434,8 @@ async def view_page_root(request: Request, page_id: int):
@router.get("/accounts", response_class=HTMLResponse)
async def accounts_page(request: Request):
"""Account management panel."""
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader("app/templates"))
from app.templating import ENV
env = ENV
sidebar = _sidebar_data(request, [])
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
with get_conn() as conn:
@@ -451,8 +448,8 @@ async def accounts_page(request: Request):
@router.get("/help", response_class=HTMLResponse)
async def help_page(request: Request):
"""Comprehensive help & documentation page."""
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader("app/templates"))
from app.templating import ENV
env = ENV
sidebar = _sidebar_data(request, [])
# Render via a block-based template so content_html lands in {% block content %}
block_tpl = env.from_string(
@@ -656,8 +653,8 @@ favorites, tags, sharing, sprints and templates; pagination (<code>?limit=&amp;o
@router.get("/accounts/settings", response_class=HTMLResponse)
async def settings_page(request: Request):
"""User settings page — profile, forges, tokens."""
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader("app/templates"))
from app.templating import ENV
env = ENV
sidebar = _sidebar_data(request, [])
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
if not user:
@@ -769,14 +766,14 @@ async def dashboard(
with get_conn() as conn:
count = conn.execute("SELECT COUNT(*) FROM users").fetchone()[0]
if count == 0:
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader("app/templates"))
from app.templating import ENV
env = ENV
template = env.get_template("landing.html")
return template.render()
except Exception:
pass
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader("app/templates"))
from app.templating import ENV
env = ENV
template = env.get_template("landing.html")
return template.render()
@@ -821,8 +818,8 @@ async def dashboard(
logger.error("Dashboard error: %s", e)
repos = []
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader("app/templates"))
from app.templating import ENV
env = ENV
sidebar = _sidebar_data(request, repos)
template = env.get_template("dashboard.html")
return template.render(request=request, repos=repos, search=search,
@@ -834,8 +831,8 @@ async def dashboard(
@router.get("/workspace", response_class=HTMLResponse)
async def workspace_page(request: Request):
"""Unified workspace showing all projects."""
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader("app/templates"))
from app.templating import ENV
env = ENV
sidebar = _sidebar_data(request, [])
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
if not user:
@@ -853,8 +850,8 @@ async def gitea_workspace_page(request: Request):
"""Gitea workspace — browse repo files."""
import json
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader("app/templates"))
from app.templating import ENV
env = ENV
sidebar = _sidebar_data(request, [])
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
if not user:
@@ -1030,8 +1027,8 @@ async def local_workspace_page(request: Request, folder: int = None):
If ?folder=ID is provided, shows that folder's contents with breadcrumb.
"""
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader("app/templates"))
from app.templating import ENV
env = ENV
sidebar = _sidebar_data(request, [])
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
if not user:
@@ -1735,8 +1732,8 @@ def _get_active_workspace(request: Request, user_id: int = None) -> dict | None:
@router.get("/workspaces", response_class=HTMLResponse)
async def workspaces_page(request: Request):
"""Workspaces list page."""
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader("app/templates"))
from app.templating import ENV
env = ENV
sidebar = _sidebar_data(request, [], include_workspace=False)
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
if not user:
@@ -1838,8 +1835,8 @@ async def select_workspace(request: Request, ws_id: int):
@router.get("/settings", response_class=HTMLResponse)
async def app_settings_page(request: Request):
"""Settings & configuration page."""
from jinja2 import Environment, FileSystemLoader
env = Environment(loader=FileSystemLoader("app/templates"))
from app.templating import ENV
env = ENV
sidebar = _sidebar_data(request, [])
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
if not user:
@@ -2156,9 +2153,8 @@ async def sidebar_workspace_tree(request: Request):
Called by appState().refreshSidebarTree() after CRUD operations
in the main content area to keep the sidebar in sync.
"""
from jinja2 import Environment, FileSystemLoader
from app.routers.board import _load_workspace_pages
from app.templating import ENV
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
if not user:
@@ -2194,7 +2190,7 @@ async def sidebar_workspace_tree(request: Request):
)
# Render the tree using the extracted macro
env = Environment(loader=FileSystemLoader("app/templates"))
env = ENV
template = env.from_string(
"{% from '_workspace_tree_macro.html' import render_workspace_tree %}"
"{{ render_workspace_tree(pages) }}"
@@ -2215,7 +2211,7 @@ async def sidebar_workspace_tree(request: Request):
@router.get("/p/{slug}", response_class=HTMLResponse)
async def public_published_page(request: Request, slug: str):
"""Serve a published page at /p/<slug> — no auth required."""
from jinja2 import Environment, FileSystemLoader
from app.templating import ENV
with get_conn() as conn:
row = conn.execute(
@@ -2235,7 +2231,7 @@ h1{font-size:3rem;opacity:.3}</style></head><body><h1>404</h1></body></html>""",
)
page = dict(row)
env = Environment(loader=FileSystemLoader("app/templates"))
env = ENV
# Convert blocks to HTML for rendering
content_html = ""