@@ -24,6 +24,53 @@ __all__ = ["create_server", "_django_ready"]
Show 2 hidden lines
+# ---------------------------------------------------------------------------+# Path containment shared by the tools that take a filesystem path
+# ---------------------------------------------------------------------------+++def _path_within(path: str, roots: "list[Any]") -> bool:+ """Whether ``path`` resolves inside one of ``roots``.++ Both sides go through ``os.path.realpath``, so ``..`` segments and symlinks
Show 14 hidden lines
+ are followed before comparing, and the comparison is+ ``os.path.commonpath`` on whole path components, so ``/app-data`` is not+ inside ``/app``. An empty ``roots`` contains nothing.+ """+ real = os.path.realpath(path)+ for root in roots:+ if not root:+ continue+ base = os.path.realpath(str(root))+ try:+ if os.path.commonpath([real, base]) == base:+ return True+ except ValueError: # different drives, or mixed absolute/relative+ continue
+ return False+++def _template_roots(engine: Any) -> "list[str]":+ """Directories ``engine``'s loaders serve templates from, plus ``BASE_DIR``."""+ from django.conf import settings++ roots: list[str] = [str(d) for d in getattr(engine, "dirs", None) or []]
Show 7 hidden lines
+ pending = list(getattr(engine, "template_loaders", None) or [])+ while pending:+ loader = pending.pop()+ pending.extend(getattr(loader, "loaders", None) or []) # cached.Loader+ get_dirs = getattr(loader, "get_dirs", None)+ if callable(get_dirs):+ try:
+ roots.extend(str(d) for d in get_dirs())+ except Exception: # noqa: BLE001 -- a loader without dirs contributes none+ pass+ base_dir = getattr(settings, "BASE_DIR", None)+ if base_dir:+ roots.append(str(base_dir))+ return roots++
Show 8 hidden lines
# --------------------------------------------------------------------------- # Template handler cross-reference (find_handlers_for_template) # ---------------------------------------------------------------------------@@ -55,16 +102,29 @@ def template_handler_report(template_path: str) -> Dict[str, Any]: return {"error": "No Django template engine is configured."} resolved_path: Optional[str] = None template = None- if os.path.isabs(template_path) and os.path.isfile(template_path):
- resolved_path = template_path+ hint = (+ "Pass either a logical name (e.g. 'demos/counter.html') or an "+ "absolute path inside the project's template directories or BASE_DIR."+ )+ if os.path.isabs(template_path):+ # Read directly only a file that resolves (symlinks and ``..``+ # followed) inside a directory the template loaders serve, or inside+ # BASE_DIR. Anything else gets the answer a missing template gets, so+ # the reply says nothing about files elsewhere on disk.+ real_path = os.path.realpath(template_path)+ if not (os.path.isfile(real_path) and _path_within(real_path, _template_roots(engine))):+ return {+ "error": "Could not resolve template %r: %s" % (template_path, template_path),+ "hint": hint,
Show 16 hidden lines
+ }+ resolved_path = real_path else: try: template = engine.get_template(template_path) except Exception as exc: # noqa: BLE001 -- any loader failure is reported to the caller return { "error": "Could not resolve template %r: %s" % (template_path, exc),- "hint": "Pass either a logical name (e.g. 'demos/counter.html') or an "- "absolute filesystem path.",+ "hint": hint, } resolved_path = getattr(getattr(template, "origin", None), "name", None) if not resolved_path or not os.path.isfile(resolved_path):@@ -411,7 +471,8 @@ def seed_fixtures(fixture_paths: list[str]) -> str: fixture_paths: Absolute or relative paths to fixture files
The repair adds _path_within and _template_roots, then restricts absolute template reads to loader-served directories or BASE_DIR. Paths outside those roots get the same response as a missing template, closing the file-existence and dj-* value leak.