From 30ba01729aef3e98d004cbfd2f77d84f860326e0 Mon Sep 17 00:00:00 2001 From: Herdiyan Adam Putra Date: Sun, 28 Jun 2026 23:16:14 +0700 Subject: [PATCH] fix(skills): prevent path traversal in LocalSkillSource MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The startsWith("references/") prefix check in LoadSkillResourceTool is bypassed by payloads like "references/../../../../etc/passwd" — the string prefix matches while the resolved path escapes skillsBasePath. Fix by calling Path.normalize() on the resolved path and asserting that it still starts with the expected base directory before any filesystem access in both findResourcePath() and listResources(). Fixes CWE-22 (Path Traversal). --- .../google/adk/skills/LocalSkillSource.java | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/com/google/adk/skills/LocalSkillSource.java b/core/src/main/java/com/google/adk/skills/LocalSkillSource.java index 4eafa4d5f..fec6f4496 100644 --- a/core/src/main/java/com/google/adk/skills/LocalSkillSource.java +++ b/core/src/main/java/com/google/adk/skills/LocalSkillSource.java @@ -45,12 +45,21 @@ public LocalSkillSource(Path skillsBasePath) { @Override public Single> listResources(String skillName, String resourceDirectory) { - Path skillDir = skillsBasePath.resolve(skillName); + Path skillDir = skillsBasePath.resolve(skillName).normalize(); if (!isDirectory(skillDir)) { return Single.error( new SkillSourceException("Skill not found: " + skillName, SKILL_NOT_FOUND)); } - Path resourceDir = skillDir.resolve(resourceDirectory); + Path resourceDir = skillDir.resolve(resourceDirectory).normalize(); + // Prevent path traversal: the resolved resource directory must remain inside + // the skill's own directory. A raw string prefix check on the input is not + // sufficient because "references/../../../../etc" bypasses it. + if (!resourceDir.startsWith(skillDir)) { + return Single.error( + new SkillSourceException( + "Path traversal detected in resource directory: " + resourceDirectory, + RESOURCE_NOT_FOUND)); + } if (!isDirectory(resourceDir)) { return Single.error( new SkillSourceException( @@ -96,7 +105,17 @@ protected Flowable> listSkills() { @Override protected Single findResourcePath(String skillName, String resourcePath) { - Path file = skillsBasePath.resolve(skillName).resolve(resourcePath); + Path base = skillsBasePath.resolve(skillName).normalize(); + Path file = base.resolve(resourcePath).normalize(); + // Enforce boundary: the resolved path must remain inside the skill's base + // directory. Without this check, a payload like + // "references/../../../../etc/passwd" passes the startsWith("references/") + // prefix check in LoadSkillResourceTool but resolves outside skillsBasePath. + if (!file.startsWith(base)) { + return Single.error( + new SkillSourceException( + "Path traversal detected: " + resourcePath, RESOURCE_NOT_FOUND)); + } if (!Files.exists(file)) { return Single.error( new SkillSourceException("Resource not found: " + file, RESOURCE_NOT_FOUND));