Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions api/routers/wiki.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from api.config import WIKI_AUTH_CODE, WIKI_AUTH_MODE, configs
from api.logger import get_logger
from api.utils import deepwiki_root
from api.schemas import (
ProcessedProjectEntry,
WikiCacheData,
Expand All @@ -34,6 +35,34 @@
router = APIRouter(tags=["wiki"])


def _allowed_local_roots() -> list[str]:
"""Base directories under which local-repository paths are permitted.

Defaults to the deepwiki data/clone root. Operators who legitimately keep
local repositories elsewhere can add roots via DEEPWIKI_ALLOWED_LOCAL_ROOTS
(os.pathsep-separated). This is the allowlist for the otherwise-unauthenticated
local-repository endpoints, which must not be able to read arbitrary paths on
the server's filesystem.
"""
roots = [deepwiki_root()]
extra = os.environ.get("DEEPWIKI_ALLOWED_LOCAL_ROOTS", "")
roots.extend(p for p in extra.split(os.pathsep) if p.strip())
return [os.path.realpath(r) for r in roots]


def _resolve_allowed_local_path(path: str) -> str | None:
"""Resolve *path* and return it only if it stays within an allowed root.

Returns the symlink-resolved path when permitted, else None. Blocks
absolute paths outside the allowlist and `..`/symlink traversal out of it.
"""
resolved = os.path.realpath(path)
for root in _allowed_local_roots():
if resolved == root or resolved.startswith(root + os.sep):
return resolved
return None


@router.post("/export/wiki")
async def post_export_wiki(request: WikiExportRequest):
"""
Expand Down Expand Up @@ -91,6 +120,18 @@ async def get_local_repo_structure(
},
)

resolved = _resolve_allowed_local_path(path)
if resolved is None:
logger.warning("Rejected out-of-root local repository path: %s", path)
return JSONResponse(
status_code=403,
content={
"error": "Path is not within an allowed local repository root. "
"Set DEEPWIKI_ALLOWED_LOCAL_ROOTS to permit additional locations."
},
)
path = resolved

if not os.path.isdir(path):
return JSONResponse(
status_code=404, content={"error": f"Directory not found: {path}"}
Expand Down