From 7b8c81ea4954afcaa0bbec4a3c38bb7ed6b218cc Mon Sep 17 00:00:00 2001 From: zumayaaustin Date: Thu, 9 Jul 2026 00:13:39 +0000 Subject: [PATCH] Resolve existing skills via iterdir match to break path-injection taint (CodeQL) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- server.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/server.py b/server.py index a2e30cc..60f87bf 100644 --- a/server.py +++ b/server.py @@ -280,6 +280,21 @@ def skill_dir_path(name: str) -> Path: raise HTTPException(400, "Invalid skill name") return candidate +def resolve_skill_dir(name: str) -> Path: + """Return the directory of an existing skill by matching ``name`` against the + actual directory entries. + + Using the entry from ``iterdir()`` (rather than a path built from ``name``) + means traversal input can never escape the skills directory. Raises 404 if no + skill matches. + """ + base = BASE_DIR / "skills" + if base.exists(): + for entry in base.iterdir(): + if entry.is_dir() and entry.name == name: + return entry + raise HTTPException(404, "Skill not found") + def skill_context_file_path(name: str, filename: str) -> Path: if not SKILL_CONTEXT_FILENAME_RE.fullmatch(filename or ""): raise HTTPException(400, "Invalid file name") @@ -309,9 +324,7 @@ def list_skills(): @app.get("/api/skills/{name}") def get_skill(name: str): - path = skill_dir_path(name) - if not path.exists(): - raise HTTPException(404, "Skill not found") + path = resolve_skill_dir(name) return { "name": name, "skill": read_file(path / "SKILL.md"), @@ -369,9 +382,7 @@ def delete_skill_context_file(name: str, filename: str): @app.post("/api/skills/{name}/run") def run_skill(name: str, req: Optional[SkillRunRequest] = None): - path = skill_dir_path(name) - if not path.exists(): - raise HTTPException(404, "Skill not found") + path = resolve_skill_dir(name) agent_choice = req.agent if req else "auto" skill_input = req.input if req else "" @@ -452,7 +463,7 @@ def run_skill(name: str, req: Optional[SkillRunRequest] = None): @app.get("/api/skills/{name}/eval") def get_skill_eval(name: str): - path = skill_dir_path(name) / "score-history.json" + path = resolve_skill_dir(name) / "score-history.json" return {"scores": load_json_file(path, default=[])} # ─── Routes: Scheduler ────────────────────────────────────────────