diff --git a/meshview/web.py b/meshview/web.py index 72beeb4..887264b 100644 --- a/meshview/web.py +++ b/meshview/web.py @@ -241,8 +241,11 @@ async def serve_page(request): if not page.endswith(".html"): page = f"{page}.html" - html_file = pathlib.Path(__file__).parent / "static" / page - if not html_file.exists(): + # aiohttp lets an encoded slash (%2F) through a single path segment, so resolve the + # final path and make sure it is still inside the static directory. + static_root = (pathlib.Path(__file__).parent / "static").resolve() + html_file = (static_root / page).resolve() + if static_root not in html_file.parents or not html_file.is_file(): raise web.HTTPNotFound(text=f"Page '{page}' not found") content = html_file.read_text(encoding="utf-8") diff --git a/meshview/web_api/api.py b/meshview/web_api/api.py index 3d94282..f811ce4 100644 --- a/meshview/web_api/api.py +++ b/meshview/web_api/api.py @@ -5,6 +5,7 @@ import json import logging import math import os +import re from aiohttp import web from sqlalchemy import func, select @@ -37,6 +38,9 @@ _LANG_CACHE = {} # Create dedicated route table for API endpoints routes = web.RouteTableDef() +# Language codes are short identifiers such as "en", "es", "pt-BR". +LANG_CODE_RE = re.compile(r"^[A-Za-z]{2,3}(?:[-_][A-Za-z0-9]{2,8})?$") + def _config_bool(section: str, key: str, default: bool = False) -> bool: return str(CONFIG.get(section, {}).get(key, default)).lower() in ("1", "true", "yes", "on") @@ -779,6 +783,10 @@ async def api_lang(request): lang_code = request.query.get("lang") or CONFIG.get("site", {}).get("language", "en") section = request.query.get("section") + # The code names a file, so it must stay a plain language code. Anything else (a path, + # an absolute filename) falls back to English just like an unknown language does. + if not LANG_CODE_RE.match(lang_code): + lang_code = "en" lang_file = os.path.join(LANG_DIR, f"{lang_code}.json") if not os.path.exists(lang_file): lang_file = os.path.join(LANG_DIR, "en.json")