HKUDS/DeepTutor · error · HTTPException

Access denied

Error message

Access denied

What it means

Raised by _safe_join_raw as a second line of defense: after resolving (raw_dir / rel_path), the result does not live under raw_dir, so the resolved target escaped the KB root (symlink, traversal, or absolute path). Unlike the 400 in _sanitize_rel_subdir this returns 403 Access denied.

Source

Thrown at deeptutor/api/routers/knowledge.py:345

    for raw_seg in str(rel_path).replace("\\", "/").split("/"):
        seg = raw_seg.strip()
        if seg in ("", "."):
            continue
        if seg == "..":
            raise HTTPException(status_code=400, detail="Invalid folder path")
        safe = _sanitize_path_segment(seg)
        if safe:
            parts.append(safe)
    return "/".join(parts)


def _safe_join_raw(raw_dir: Path, rel_path: str) -> Path:
    """Resolve ``rel_path`` under ``raw_dir``, rejecting traversal."""
    target = (raw_dir / rel_path).resolve()
    try:
        target.relative_to(raw_dir.resolve())
    except ValueError as exc:
        raise HTTPException(status_code=403, detail="Access denied") from exc
    return target


def _save_uploaded_files(
    files: list[UploadFile],
    target_dir: Path,
    allowed_extensions: set[str] | None = None,
    kb_name: str | None = None,
    rel_paths: list[str] | None = None,
    dest_subdir: str = "",
) -> tuple[list[str], list[str]]:
    """
    Save uploaded files to the local raw/ directory.

    When PocketBase is enabled and ``kb_name`` is supplied, each file is also
    uploaded to the PocketBase knowledge_bases record as a file attachment
    (best-effort — local write is always the primary path).
    """

View on GitHub (pinned to 3e82f13042)

Solutions

  1. Audit the KB raw directory for symlinks pointing outside it (find . -type l -exec readlink {} \;)
  2. Ensure the client only sends simple relative folder names
  3. Re-run with the same input after removing offending symlinks; keep the segment-level sanitizer upstream of this call

Example fix

# before
rel = '../../../../home/user/secret'
# after
rel = 'semester1/cs101'
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

def joins_inside(root: Path, rel: str) -> bool:
    try:
        (root / rel).resolve().relative_to(root.resolve())
        return True
    except ValueError:
        return False

Prevention

When it happens

Trigger: create_kb_folder or move_kb_file where rel_path contains traversal that survived sanitization, or where a symlink inside raw_dir resolves outside it; passing an absolute path that resolve() anchors elsewhere.

Common situations: Symlinks inside the KB directory pointing to external locations; race between check and use; crafted payloads bypassing segment-level checks.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


AI-assisted analysis of HKUDS/DeepTutor@3e82f13042 (2026-08-27). Data as JSON: /api/errors/29977aa572a4d5dc. Report an issue: GitHub.