Skip to content

Commit f10eceb

Browse files
committed
fix(agents): prevent path traversal in AgentTool config_path resolution
Reject absolute config_path values and require the resolved sub-agent config path to stay within the agent directory, with unit tests covering absolute-path rejection, .. traversal rejection, and valid in-bounds resolution.
1 parent 5ee51fd commit f10eceb

2 files changed

Lines changed: 180 additions & 4 deletions

File tree

core/src/main/java/com/google/adk/agents/ConfigAgentUtils.java

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -247,12 +247,18 @@ private static BaseAgent resolveSubAgentFromConfigPath(
247247
BaseAgentConfig.AgentRefConfig subAgentConfig, Path configDir) throws ConfigurationException {
248248

249249
String configPath = subAgentConfig.configPath().trim();
250-
Path subAgentConfigPath;
251250

252251
if (Path.of(configPath).isAbsolute()) {
253-
subAgentConfigPath = Path.of(configPath);
254-
} else {
255-
subAgentConfigPath = configDir.resolve(configPath);
252+
throw new ConfigurationException(
253+
"Absolute paths are not allowed in AgentTool config_path: " + configPath);
254+
}
255+
256+
Path subAgentConfigPath = configDir.resolve(configPath).normalize().toAbsolutePath();
257+
Path canonicalConfigDir = configDir.normalize().toAbsolutePath();
258+
259+
if (!subAgentConfigPath.startsWith(canonicalConfigDir)) {
260+
throw new ConfigurationException(
261+
"Path traversal detected: config_path resolves outside agent directory: " + configPath);
256262
}
257263

258264
if (!Files.exists(subAgentConfigPath)) {

core/src/test/java/com/google/adk/agents/ConfigAgentUtilsTest.java

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1381,6 +1381,176 @@ public void testCallbackRefAccessors() {
13811381
assertThat(callbackRef.name()).isEqualTo("updated-name");
13821382
}
13831383

1384+
@Test
1385+
public void resolveSubAgents_withAbsoluteConfigPath_throwsConfigurationException()
1386+
throws IOException {
1387+
// A secret file outside the agent directory that an attacker might try to read.
1388+
File secretFile = tempFolder.newFile("secret.yaml");
1389+
Files.writeString(
1390+
secretFile.toPath(),
1391+
"""
1392+
agent_class: LlmAgent
1393+
name: secret_agent
1394+
description: A secret agent that should not be loadable via absolute path
1395+
instruction: secret instruction
1396+
""");
1397+
String absoluteConfigPath = secretFile.getAbsolutePath();
1398+
1399+
File mainAgentFile = tempFolder.newFile("main_agent_absolute.yaml");
1400+
Files.writeString(
1401+
mainAgentFile.toPath(),
1402+
String.format(
1403+
"""
1404+
agent_class: LlmAgent
1405+
name: main_agent
1406+
description: Main agent referencing an absolute config_path
1407+
instruction: You are a main agent
1408+
sub_agents:
1409+
- name: secret_agent
1410+
config_path: %s
1411+
""",
1412+
absoluteConfigPath));
1413+
1414+
ConfigurationException exception =
1415+
assertThrows(
1416+
ConfigurationException.class,
1417+
() -> ConfigAgentUtils.fromConfig(mainAgentFile.getAbsolutePath()));
1418+
1419+
assertThat(exception).hasMessageThat().contains("Failed to create agent from config");
1420+
StringBuilder messages = new StringBuilder();
1421+
Throwable t = exception.getCause();
1422+
while (t != null) {
1423+
messages.append(t.getMessage()).append("\n");
1424+
t = t.getCause();
1425+
}
1426+
assertThat(messages.toString())
1427+
.contains("Absolute paths are not allowed in AgentTool config_path: " + absoluteConfigPath);
1428+
}
1429+
1430+
@Test
1431+
public void resolveSubAgents_withTraversalConfigPath_throwsConfigurationException()
1432+
throws IOException {
1433+
// The agent config lives in a nested subdirectory so that "../../" escapes the agent
1434+
// directory. The traversal target is a real file to prove the rejection happens before any
1435+
// file read.
1436+
File secretFile = tempFolder.newFile("outside_secret.yaml");
1437+
Files.writeString(
1438+
secretFile.toPath(),
1439+
"""
1440+
agent_class: LlmAgent
1441+
name: outside_secret_agent
1442+
description: A file outside the agent directory
1443+
instruction: secret instruction
1444+
""");
1445+
1446+
File agentDir = tempFolder.newFolder("nested", "agents");
1447+
File mainAgentFile = new File(agentDir, "main_agent_traversal.yaml");
1448+
Files.writeString(
1449+
mainAgentFile.toPath(),
1450+
"""
1451+
agent_class: LlmAgent
1452+
name: main_agent
1453+
description: Main agent referencing a traversal config_path
1454+
instruction: You are a main agent
1455+
sub_agents:
1456+
- name: outside_secret_agent
1457+
config_path: ../../outside_secret.yaml
1458+
""");
1459+
1460+
ConfigurationException exception =
1461+
assertThrows(
1462+
ConfigurationException.class,
1463+
() -> ConfigAgentUtils.fromConfig(mainAgentFile.getAbsolutePath()));
1464+
1465+
assertThat(exception).hasMessageThat().contains("Failed to create agent from config");
1466+
StringBuilder messages = new StringBuilder();
1467+
Throwable t = exception.getCause();
1468+
while (t != null) {
1469+
messages.append(t.getMessage()).append("\n");
1470+
t = t.getCause();
1471+
}
1472+
assertThat(messages.toString())
1473+
.contains(
1474+
"Path traversal detected: config_path resolves outside agent directory:"
1475+
+ " ../../outside_secret.yaml");
1476+
}
1477+
1478+
@Test
1479+
public void resolveSubAgents_withRelativeConfigPathWithinAgentDir_resolvesSuccessfully()
1480+
throws IOException, ConfigurationException {
1481+
// A normal relative config_path that stays within the agent directory must still work.
1482+
File subAgentFile = tempFolder.newFile("normal_sub_agent.yaml");
1483+
Files.writeString(
1484+
subAgentFile.toPath(),
1485+
"""
1486+
agent_class: LlmAgent
1487+
name: normal_sub_agent
1488+
description: A normal subagent
1489+
instruction: You are a helpful subagent
1490+
""");
1491+
1492+
File mainAgentFile = tempFolder.newFile("main_agent_relative.yaml");
1493+
Files.writeString(
1494+
mainAgentFile.toPath(),
1495+
"""
1496+
agent_class: LlmAgent
1497+
name: main_agent
1498+
description: Main agent with a normal relative config_path
1499+
instruction: You are a main agent
1500+
sub_agents:
1501+
- name: normal_sub_agent
1502+
config_path: normal_sub_agent.yaml
1503+
""");
1504+
1505+
BaseAgent mainAgent = ConfigAgentUtils.fromConfig(mainAgentFile.getAbsolutePath());
1506+
1507+
assertThat(mainAgent.name()).isEqualTo("main_agent");
1508+
assertThat(mainAgent.subAgents()).hasSize(1);
1509+
BaseAgent subAgent = mainAgent.subAgents().get(0);
1510+
assertThat(subAgent.name()).isEqualTo("normal_sub_agent");
1511+
assertThat(subAgent).isInstanceOf(LlmAgent.class);
1512+
}
1513+
1514+
@Test
1515+
public void resolveSubAgents_withTraversalThatStaysWithinAgentDir_resolvesSuccessfully()
1516+
throws IOException, ConfigurationException {
1517+
// A config_path containing ".." that still normalizes to a location inside the agent
1518+
// directory must be accepted (the containment check is on the normalized path, not a naive
1519+
// ".." substring match).
1520+
File childDir = tempFolder.newFolder("child");
1521+
File subAgentFile = new File(childDir, "nested_sub_agent.yaml");
1522+
Files.writeString(
1523+
subAgentFile.toPath(),
1524+
"""
1525+
agent_class: LlmAgent
1526+
name: nested_sub_agent
1527+
description: A nested subagent reached via an in-bounds relative path
1528+
instruction: You are a helpful subagent
1529+
""");
1530+
1531+
File mainAgentFile = tempFolder.newFile("main_agent_inbounds_traversal.yaml");
1532+
// child/../child/nested_sub_agent.yaml normalizes back into the agent directory.
1533+
Files.writeString(
1534+
mainAgentFile.toPath(),
1535+
"""
1536+
agent_class: LlmAgent
1537+
name: main_agent
1538+
description: Main agent with an in-bounds traversal config_path
1539+
instruction: You are a main agent
1540+
sub_agents:
1541+
- name: nested_sub_agent
1542+
config_path: child/../child/nested_sub_agent.yaml
1543+
""");
1544+
1545+
BaseAgent mainAgent = ConfigAgentUtils.fromConfig(mainAgentFile.getAbsolutePath());
1546+
1547+
assertThat(mainAgent.name()).isEqualTo("main_agent");
1548+
assertThat(mainAgent.subAgents()).hasSize(1);
1549+
BaseAgent subAgent = mainAgent.subAgents().get(0);
1550+
assertThat(subAgent.name()).isEqualTo("nested_sub_agent");
1551+
assertThat(subAgent).isInstanceOf(LlmAgent.class);
1552+
}
1553+
13841554
@Test
13851555
public void fromConfig_validYamlLoopAgent_createsLoopAgent()
13861556
throws IOException, ConfigurationException {

0 commit comments

Comments
 (0)