Skip to content

Commit 30ba017

Browse files
committed
fix(skills): prevent path traversal in LocalSkillSource
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).
1 parent 587073a commit 30ba017

1 file changed

Lines changed: 22 additions & 3 deletions

File tree

core/src/main/java/com/google/adk/skills/LocalSkillSource.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,21 @@ public LocalSkillSource(Path skillsBasePath) {
4545

4646
@Override
4747
public Single<ImmutableList<String>> listResources(String skillName, String resourceDirectory) {
48-
Path skillDir = skillsBasePath.resolve(skillName);
48+
Path skillDir = skillsBasePath.resolve(skillName).normalize();
4949
if (!isDirectory(skillDir)) {
5050
return Single.error(
5151
new SkillSourceException("Skill not found: " + skillName, SKILL_NOT_FOUND));
5252
}
53-
Path resourceDir = skillDir.resolve(resourceDirectory);
53+
Path resourceDir = skillDir.resolve(resourceDirectory).normalize();
54+
// Prevent path traversal: the resolved resource directory must remain inside
55+
// the skill's own directory. A raw string prefix check on the input is not
56+
// sufficient because "references/../../../../etc" bypasses it.
57+
if (!resourceDir.startsWith(skillDir)) {
58+
return Single.error(
59+
new SkillSourceException(
60+
"Path traversal detected in resource directory: " + resourceDirectory,
61+
RESOURCE_NOT_FOUND));
62+
}
5463
if (!isDirectory(resourceDir)) {
5564
return Single.error(
5665
new SkillSourceException(
@@ -96,7 +105,17 @@ protected Flowable<SkillMdPath<Path>> listSkills() {
96105

97106
@Override
98107
protected Single<Path> findResourcePath(String skillName, String resourcePath) {
99-
Path file = skillsBasePath.resolve(skillName).resolve(resourcePath);
108+
Path base = skillsBasePath.resolve(skillName).normalize();
109+
Path file = base.resolve(resourcePath).normalize();
110+
// Enforce boundary: the resolved path must remain inside the skill's base
111+
// directory. Without this check, a payload like
112+
// "references/../../../../etc/passwd" passes the startsWith("references/")
113+
// prefix check in LoadSkillResourceTool but resolves outside skillsBasePath.
114+
if (!file.startsWith(base)) {
115+
return Single.error(
116+
new SkillSourceException(
117+
"Path traversal detected: " + resourcePath, RESOURCE_NOT_FOUND));
118+
}
100119
if (!Files.exists(file)) {
101120
return Single.error(
102121
new SkillSourceException("Resource not found: " + file, RESOURCE_NOT_FOUND));

0 commit comments

Comments
 (0)