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
43 changes: 29 additions & 14 deletions core/src/main/java/com/google/adk/agents/ConfigAgentUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -250,21 +250,23 @@ private static BaseAgent resolveSubAgentFromConfigPath(
Path subAgentConfigPath;

if (Path.of(configPath).isAbsolute()) {
subAgentConfigPath = Path.of(configPath);
} else {
subAgentConfigPath = configDir.resolve(configPath);
throw new ConfigurationException(
"Absolute paths are not allowed in AgentTool config_path: " + configPath);
}

// Warn when the resolved config path escapes the agent's base directory. For backward
// compatibility this is still allowed, but the behavior is deprecated and will be disallowed
// in a future release.
Path resolvedConfigPath = subAgentConfigPath.normalize().toAbsolutePath();
Path baseDir = configDir.normalize().toAbsolutePath();
if (!resolvedConfigPath.startsWith(baseDir)) {
logger.warn(
"AgentTool config_path '{}' accesses a path outside the agent base directory; this"
+ " behavior is deprecated and will be disallowed in a future release.",
configPath);
subAgentConfigPath = configDir.resolve(configPath);

// Reject config paths that resolve outside the agent's base directory. Both sides are
// resolved to their real (symlink-free) absolute form where the paths exist, so a symlink
// inside the base directory cannot be used to escape it; if a path does not yet exist,
// the lexical absolute/normalized form is used, which still correctly rejects a literal
// "../" escape.
Path resolvedConfigPath = resolveReal(subAgentConfigPath.normalize().toAbsolutePath());
Path baseDir = resolveReal(configDir.normalize().toAbsolutePath());
if (!resolvedConfigPath.startsWith(baseDir) && !resolvedConfigPath.equals(baseDir)) {
throw new ConfigurationException(
"Path traversal detected: AgentTool config_path '"
+ configPath
+ "' resolves outside the agent base directory.");
}

if (!Files.exists(subAgentConfigPath)) {
Expand All @@ -280,6 +282,19 @@ private static BaseAgent resolveSubAgentFromConfigPath(
}
}

/**
* Resolves symlinks in {@code path} where the path exists; falls back to the given (already
* normalized, absolute) path unchanged if it does not exist, so that a missing file is reported
* as not-found rather than misclassified as a traversal.
*/
private static Path resolveReal(Path path) {
try {
return path.toRealPath();
} catch (IOException e) {
return path;
}
}

/**
* Load configuration from a YAML file path as a specific type.
*
Expand Down
58 changes: 58 additions & 0 deletions core/src/test/java/com/google/adk/agents/ConfigAgentUtilsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,64 @@ public void fromConfig_withSubAgents_createsHierarchy()
assertThat(llmSubAgent.instruction().toString()).contains("helpful subagent");
}

@Test
public void fromConfig_subAgentConfigPathTraversal_throwsConfigurationException()
throws IOException {
// A file OUTSIDE the agent's own config directory, standing in for another tenant's or the
// host's data that config_path should never be able to reach.
File outsideDir = tempFolder.newFolder("outside");
File secretFile = new File(outsideDir, "secret.yaml");
Files.writeString(secretFile.toPath(), "agent_class: LlmAgent\nname: leaked\n");

File agentDir = tempFolder.newFolder("agents", "main");
File mainAgentFile = new File(agentDir, "main_agent.yaml");
Files.writeString(
mainAgentFile.toPath(),
"""
agent_class: LlmAgent
name: main_agent
description: Main agent with a traversing subagent reference
instruction: You are a main agent
sub_agents:
- name: escaping_subagent
config_path: ../../outside/secret.yaml
""");

ConfigurationException exception =
assertThrows(
ConfigurationException.class,
() -> ConfigAgentUtils.fromConfig(mainAgentFile.getAbsolutePath()));
assertThat(exception).hasMessageThat().contains("Path traversal detected");
}

@Test
public void fromConfig_subAgentAbsoluteConfigPath_throwsConfigurationException()
throws IOException {
File outsideFile = tempFolder.newFile("absolute_target.yaml");
Files.writeString(outsideFile.toPath(), "agent_class: LlmAgent\nname: leaked\n");

File mainAgentFile = tempFolder.newFile("main_agent.yaml");
Files.writeString(
mainAgentFile.toPath(),
String.format(
"""
agent_class: LlmAgent
name: main_agent
description: Main agent with an absolute-path subagent reference
instruction: You are a main agent
sub_agents:
- name: absolute_subagent
config_path: %s
""",
outsideFile.getAbsolutePath()));

ConfigurationException exception =
assertThrows(
ConfigurationException.class,
() -> ConfigAgentUtils.fromConfig(mainAgentFile.getAbsolutePath()));
assertThat(exception).hasMessageThat().contains("Absolute paths are not allowed");
}

@Test
public void resolveSubAgents_missingConfigPath_throwsConfigurationException() throws IOException {
File mainAgentFile = tempFolder.newFile("main_agent.yaml");
Expand Down