Skip to content
Open
Show file tree
Hide file tree
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
36 changes: 36 additions & 0 deletions core/src/main/java/com/google/adk/skills/LocalSkillSource.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ public LocalSkillSource(Path skillsBasePath) {

@Override
public Single<ImmutableList<String>> listResources(String skillName, String resourceDirectory) {
try {
validatePathWithinBase(skillsBasePath, skillName);
validatePathWithinBase(skillsBasePath.resolve(skillName), resourceDirectory);
} catch (SkillSourceException e) {
return Single.error(e);
}
Path skillDir = skillsBasePath.resolve(skillName);
if (!isDirectory(skillDir)) {
return Single.error(new SkillSourceException("Skill not found: " + skillName));
Expand Down Expand Up @@ -86,6 +92,12 @@ protected Flowable<SkillMdPath> listSkills() {

@Override
protected Single<Path> findResourcePath(String skillName, String resourcePath) {
try {
validatePathWithinBase(skillsBasePath, skillName);
validatePathWithinBase(skillsBasePath.resolve(skillName), resourcePath);
} catch (SkillSourceException e) {
return Single.error(e);
}
Path file = skillsBasePath.resolve(skillName).resolve(resourcePath);
if (!Files.exists(file)) {
return Single.error(new SkillSourceException("Resource not found: " + file));
Expand All @@ -95,6 +107,11 @@ protected Single<Path> findResourcePath(String skillName, String resourcePath) {

@Override
protected Single<Path> findSkillMdPath(String skillName) {
try {
validatePathWithinBase(skillsBasePath, skillName);
} catch (SkillSourceException e) {
return Single.error(e);
}
Path skillDir = skillsBasePath.resolve(skillName);
if (!isDirectory(skillDir)) {
return Single.error(new SkillSourceException("Skill directory not found: " + skillName));
Expand All @@ -115,4 +132,23 @@ private Optional<Path> findSkillMd(Path dir) {
.or(() -> Optional.of(dir.resolve("skill.md")))
.filter(Files::exists);
}

/**
* Validates that {@code component} does not escape {@code base} when resolved against it.
*
* @throws SkillSourceException if the resolved path would be outside {@code base}
*/
private static void validatePathWithinBase(Path base, String component)
throws SkillSourceException {
if (Path.of(component).isAbsolute()) {
throw new SkillSourceException("Absolute paths are not allowed: " + component);
}
Path normalizedBase = base.normalize().toAbsolutePath();
Path resolved = base.resolve(component).normalize().toAbsolutePath();
if (!resolved.startsWith(normalizedBase)) {
throw new SkillSourceException(
"Path traversal detected; component must remain within its parent directory: "
+ component);
}
}
}
66 changes: 66 additions & 0 deletions core/src/test/java/com/google/adk/skills/LocalSkillSourceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -250,4 +250,70 @@ public void testListSkillMdPaths_skillSourceException() throws IOException {
RuntimeException exception = assertThrows(RuntimeException.class, single::blockingGet);
assertThat(exception).hasCauseThat().isInstanceOf(SkillSourceException.class);
}

@Test
public void testLoadResource_pathTraversalInSkillName() throws IOException {
Path skillsBase = tempFolder.getRoot().toPath().resolve("skills");
Files.createDirectory(skillsBase);

SkillSource source = new LocalSkillSource(skillsBase);
var single = source.loadResource("../other-dir", "file.txt");
RuntimeException exception = assertThrows(RuntimeException.class, single::blockingGet);
assertThat(exception).hasCauseThat().isInstanceOf(SkillSourceException.class);
assertThat(exception).hasCauseThat().hasMessageThat().contains("Path traversal detected");
}

@Test
public void testLoadResource_pathTraversalInResourcePath() throws IOException {
Path skillsBase = tempFolder.getRoot().toPath().resolve("skills");
Files.createDirectory(skillsBase);
Path skillDir = skillsBase.resolve("my-skill");
Files.createDirectory(skillDir);

SkillSource source = new LocalSkillSource(skillsBase);
var single = source.loadResource("my-skill", "../../../etc/passwd");
RuntimeException exception = assertThrows(RuntimeException.class, single::blockingGet);
assertThat(exception).hasCauseThat().isInstanceOf(SkillSourceException.class);
assertThat(exception).hasCauseThat().hasMessageThat().contains("Path traversal detected");
}

@Test
public void testLoadResource_absoluteResourcePath() throws IOException {
Path skillsBase = tempFolder.getRoot().toPath().resolve("skills");
Files.createDirectory(skillsBase);
Path skillDir = skillsBase.resolve("my-skill");
Files.createDirectory(skillDir);

SkillSource source = new LocalSkillSource(skillsBase);
var single = source.loadResource("my-skill", "/etc/passwd");
RuntimeException exception = assertThrows(RuntimeException.class, single::blockingGet);
assertThat(exception).hasCauseThat().isInstanceOf(SkillSourceException.class);
assertThat(exception).hasCauseThat().hasMessageThat().contains("Absolute paths are not allowed");
}

@Test
public void testListResources_pathTraversalInSkillName() throws IOException {
Path skillsBase = tempFolder.getRoot().toPath().resolve("skills");
Files.createDirectory(skillsBase);

SkillSource source = new LocalSkillSource(skillsBase);
var single = source.listResources("../../etc", "passwd");
RuntimeException exception = assertThrows(RuntimeException.class, single::blockingGet);
assertThat(exception).hasCauseThat().isInstanceOf(SkillSourceException.class);
assertThat(exception).hasCauseThat().hasMessageThat().contains("Path traversal detected");
}

@Test
public void testListResources_pathTraversalInResourceDirectory() throws IOException {
Path skillsBase = tempFolder.getRoot().toPath().resolve("skills");
Files.createDirectory(skillsBase);
Path skillDir = skillsBase.resolve("my-skill");
Files.createDirectory(skillDir);

SkillSource source = new LocalSkillSource(skillsBase);
var single = source.listResources("my-skill", "../other-skill");
RuntimeException exception = assertThrows(RuntimeException.class, single::blockingGet);
assertThat(exception).hasCauseThat().isInstanceOf(SkillSourceException.class);
assertThat(exception).hasCauseThat().hasMessageThat().contains("Path traversal detected");
}
}