Keep /api/lang and the static page route inside their directories (#173)

* Update CONTRIBUTING.md

* Update CONTRIBUTING.md

* Keep /api/lang and the static page route inside their directories

Two routes built a filename from request input without checking where it
ended up:

- /api/lang?lang=... joined the value onto the lang directory and added
  ".json". os.path.join discards the directory when the value is an
  absolute path, so ?lang=/some/where/file returned /some/where/file.json.
  Any .json file the server process can read was served.
- /{page} appended ".html" and read static/<page>. aiohttp passes an
  encoded slash (%2F) through a single path segment and unquotes it, so
  /..%2F..%2F..%2Fx read ../../../x.html relative to the static directory.

The lang route now only accepts a plain language code (en, es, pt-BR, ...)
and falls back to English otherwise, exactly as it already did for unknown
languages. The page route resolves the final path and requires it to be a
file inside meshview/static, the same check /docs/{doc} already does.

Co-Authored-By: Claude Mythos 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Pablo Revilla <pablorevilla@gmail.com>
Co-authored-by: Nathan <nathan@stonekitty.net>
Co-authored-by: Claude Mythos 5.1 <noreply@anthropic.com>
This commit is contained in:
authored and GitHub committed 2026-10-14 07:22:37 -07:00
1 parent 0109d68c1b
commit 5214b233c1
2 files changed
+13 -2

No files matched your search

+5 -2
View File
@@ -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")
+8
View File
@@ -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")